Skip to content

Commit

Permalink
Merge pull request #15 from crytic/1-add-size-to-status-table
Browse files Browse the repository at this point in the history
1 add size to status table
  • Loading branch information
bohendo authored Oct 5, 2023
2 parents 49d7ed8 + ff62912 commit 6353b73
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 57 deletions.
3 changes: 1 addition & 2 deletions cmd/cloudexec/clean.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import (
"github.com/crytic/cloudexec/pkg/s3"
)

func ConfirmDeleteDroplets(config config.Config, userName string, instanceToJobs map[int64][]int64) error {
dropletName := fmt.Sprintf("cloudexec-%v", userName)
func ConfirmDeleteDroplets(config config.Config, dropletName string, instanceToJobs map[int64][]int64) error {
instances, err := do.GetDropletsByName(config, dropletName)
if err != nil {
return fmt.Errorf("Failed to get droplet by name: %w", err)
Expand Down
3 changes: 1 addition & 2 deletions cmd/cloudexec/launch.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,7 @@ func Launch(user *user.User, config config.Config, dropletSize string, dropletRe
updatedAt := time.Now().Unix()
for i, job := range newState.Jobs {
if job.ID == thisJobId {
newState.Jobs[i].InstanceID = droplet.ID
newState.Jobs[i].InstanceIP = droplet.IP
newState.Jobs[i].Droplet = droplet
newState.Jobs[i].UpdatedAt = updatedAt
}
}
Expand Down
54 changes: 6 additions & 48 deletions cmd/cloudexec/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@ import (
"os"
"os/user"
"strconv"
"time"

do "github.com/crytic/cloudexec/pkg/digitalocean"
"github.com/crytic/cloudexec/pkg/ssh"
"github.com/crytic/cloudexec/pkg/state"
"github.com/olekukonko/tablewriter"
"github.com/urfave/cli/v2"
)

Expand All @@ -30,9 +28,10 @@ func main() {
fmt.Printf("Failed to get current user: %v", err)
os.Exit(1)
}
userName := user.Username
username := user.Username
// TODO: sanitize username usage in bucketname
bucketName := fmt.Sprintf("cloudexec-%s", userName)
bucketName := fmt.Sprintf("cloudexec-%s", username)
dropletName := fmt.Sprintf("cloudexec-%v", username)

// Attempt to load the configuration
config, configErr := LoadConfig(configFilePath)
Expand Down Expand Up @@ -211,7 +210,7 @@ func main() {
return err
}

err = ConfirmDeleteDroplets(config, userName, instanceToJobs)
err = ConfirmDeleteDroplets(config, dropletName, instanceToJobs)
if err != nil {
return err
}
Expand Down Expand Up @@ -260,7 +259,7 @@ func main() {
if err != nil {
return err
}
err = ConfirmDeleteDroplets(config, userName, instanceToJobs)
err = ConfirmDeleteDroplets(config, username, instanceToJobs)
if err != nil {
return err
}
Expand Down Expand Up @@ -346,54 +345,13 @@ func main() {
if err != nil {
return err
}

existingState, err := state.GetState(config, bucketName)
if err != nil {
return err
}

// Print the status of each job using tablewriter
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Job ID", "Status", "Droplet ID", "Droplet IP", "Started At", "Updated At", "Completed At"})

showAll := c.Bool("all")
formatDate := func(timestamp int64) string {
if timestamp == 0 {
return ""
}
return time.Unix(timestamp, 0).Format("2006-01-02 15:04:05")
}

formatInt := func(i int64) string {
if i == 0 {
return ""
}
return strconv.Itoa(int(i))
}

// Find the latest completed job
latestCompletedJob, err := state.GetLatestCompletedJob(bucketName, existingState)
err = PrintStatus(config, bucketName, showAll)
if err != nil {
return err
}

for _, job := range existingState.Jobs {
if showAll || (job.Status == state.Running || job.Status == state.Provisioning) || (latestCompletedJob != nil && job.ID == latestCompletedJob.ID) {
table.Append([]string{
strconv.Itoa(int(job.ID)),
string(job.Status),
formatInt(job.InstanceID),
job.InstanceIP,
formatDate(job.StartedAt),
formatDate(job.UpdatedAt),
formatDate(job.CompletedAt),
})
}
}

table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetRowLine(true)
table.Render()
return nil
},
},
Expand Down
100 changes: 100 additions & 0 deletions cmd/cloudexec/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package main

import (
"fmt"
"os"
"strconv"
"time"

"github.com/crytic/cloudexec/pkg/config"
"github.com/crytic/cloudexec/pkg/state"
"github.com/olekukonko/tablewriter"
)

func PrintStatus(config config.Config, bucketName string, showAll bool) error {

existingState, err := state.GetState(config, bucketName)
if err != nil {
return err
}

// Print the status of each job using tablewriter
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Job ID", "Status", "Droplet IP", "Memory", "CPUs", "Disk", "Started At", "Updated At", "Time Elapsed", "Hourly Cost", "Total Cost"})

formatDate := func(timestamp int64) string {
if timestamp == 0 {
return ""
}
return time.Unix(timestamp, 0).Format("2006-01-02 15:04:05")
}

formatElapsedTime := func(seconds int64) string {
const (
minute = 60
hour = minute * 60
day = hour * 24
week = day * 7
)
switch {
case seconds < minute*2:
return fmt.Sprintf("%d seconds", seconds)
case seconds < hour*2:
return fmt.Sprintf("%d minutes", seconds/minute)
case seconds < day*2:
return fmt.Sprintf("%d hours", seconds/hour)
case seconds < week*2:
return fmt.Sprintf("%d days", seconds/day)
default:
return fmt.Sprintf("%d weeks", seconds/week)
}
}

formatInt := func(i int64) string {
return strconv.Itoa(int(i))
}

formatFloat := func(f float64) string {
return strconv.FormatFloat(f, 'f', 4, 64)
}

// Find the latest completed job
latestCompletedJob, err := state.GetLatestCompletedJob(bucketName, existingState)
if err != nil {
return err
}

for _, job := range existingState.Jobs {
if showAll || (job.Status == state.Running || job.Status == state.Provisioning) || (latestCompletedJob != nil && job.ID == latestCompletedJob.ID) {

latestUpdate := func() int64 {
if job.CompletedAt == 0 {
return job.UpdatedAt
}
return job.CompletedAt
}()
elapsedTime := int64(latestUpdate - job.StartedAt)
totalCost := float64(elapsedTime) / float64(3600) * job.Droplet.Size.HourlyCost

table.Append([]string{
strconv.Itoa(int(job.ID)),
string(job.Status),
job.Droplet.IP,
formatInt(job.Droplet.Size.Memory) + " MB",
formatInt(job.Droplet.Size.CPUs),
formatInt(job.Droplet.Size.Disk) + " GB",
formatDate(job.StartedAt),
formatDate(job.UpdatedAt),
formatElapsedTime(elapsedTime),
"$" + formatFloat(job.Droplet.Size.HourlyCost),
"$" + formatFloat(totalCost),
})

}
}

table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetRowLine(true)
table.Render()
return nil
}
4 changes: 2 additions & 2 deletions cmd/cloudexec/user_data.sh.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ cleanup() {
echo "Uploading results..."
s3cmd put -r ~/output/* "s3://${BUCKET_NAME}/job-${JOB_ID}/output/"
else
echo "Skipping results upload, no files found in ~/ouput"
echo "Skipping results upload, no files found in ~/output"
fi

if [[ -s ${stdout_log} ]]; then
Expand Down Expand Up @@ -256,12 +256,12 @@ while true; do
exit_code="$(cat "${exit_code_flag}")"
echo
echo "CloudExec process has completed with exit code ${exit_code}"
COMPLETED=true
if [[ ${exit_code} == "0" ]]; then
update_state "completed"
else
update_state "failed"
fi
COMPLETED=true
# Remove the done flag temp file
rm "${exit_code_flag}"
break
Expand Down
50 changes: 50 additions & 0 deletions pkg/digitalocean/digitalocean.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,19 @@ import (
"github.com/crytic/cloudexec/pkg/s3"
)

type Size struct {
CPUs int64
Disk int64
Memory int64
HourlyCost float64
}

type Droplet struct {
Name string
ID int64
IP string
Created string
Size Size
}

type Snapshot struct {
Expand Down Expand Up @@ -190,6 +198,12 @@ func CreateDroplet(config config.Config, username string, region string, size st
newDroplet = doDroplet
}

droplet.Size = Size{
CPUs: int64(newDroplet.Vcpus),
Disk: int64(newDroplet.Disk),
Memory: int64(newDroplet.Memory),
HourlyCost: float64(newDroplet.Size.PriceHourly),
}
droplet.Created = newDroplet.Created
droplet.Name = newDroplet.Name
droplet.ID = int64(newDroplet.ID)
Expand All @@ -201,6 +215,36 @@ func CreateDroplet(config config.Config, username string, region string, size st
return droplet, nil
}

func GetDropletById(config config.Config, id int64) (Droplet, error) {
// create a client
doClient, err := initializeDOClient(config.DigitalOcean.ApiKey)
if err != nil {
return Droplet{}, err
}

dropletInfo, _, err := doClient.Droplets.Get(context.TODO(), int(id))
if err != nil {
return Droplet{}, fmt.Errorf("Failed to get droplet by id: %v", err)
}
pubIp, err := dropletInfo.PublicIPv4()
if err != nil {
return Droplet{}, fmt.Errorf("Failed to fetch droplet IP: %w", err)
}

return Droplet{
Name: dropletInfo.Name,
ID: int64(dropletInfo.ID),
IP: pubIp,
Created: dropletInfo.Created,
Size: Size{
CPUs: int64(dropletInfo.Vcpus),
Disk: int64(dropletInfo.Disk),
Memory: int64(dropletInfo.Memory),
HourlyCost: float64(dropletInfo.Size.PriceHourly),
},
}, nil
}

// GetDropletsByName returns a list of droplets with the given tag using a godo client
func GetDropletsByName(config config.Config, dropletName string) ([]Droplet, error) {
var droplets []Droplet
Expand Down Expand Up @@ -228,6 +272,12 @@ func GetDropletsByName(config config.Config, dropletName string) ([]Droplet, err
ID: int64(droplet.ID),
IP: pubIp,
Created: droplet.Created,
Size: Size{
CPUs: int64(droplet.Vcpus),
Disk: int64(droplet.Disk),
Memory: int64(droplet.Memory),
HourlyCost: float64(droplet.Size.PriceHourly),
},
})
}

Expand Down
9 changes: 6 additions & 3 deletions pkg/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

"github.com/crytic/cloudexec/pkg/config"
do "github.com/crytic/cloudexec/pkg/digitalocean"
"github.com/crytic/cloudexec/pkg/s3"
)

Expand All @@ -27,10 +28,9 @@ type JobInfo struct {
StartedAt int64 `json:"started_at"` // Unix timestamp
CompletedAt int64 `json:"completed_at"`
UpdatedAt int64 `json:"updated_at"`
InstanceID int64 `json:"instance_id"`
Status JobStatus `json:"status"`
InstanceIP string `json:"instance_ip"`
Delete bool
Droplet do.Droplet `json:"droplet"`
}

type State struct {
Expand Down Expand Up @@ -210,7 +210,10 @@ func GetJobIdsByInstance(config config.Config, bucketName string) (map[int64][]i
return instanceToJobIds, nil
}
for _, job := range existingState.Jobs {
instanceToJobIds[job.InstanceID] = append(instanceToJobIds[job.InstanceID], job.ID)
if job.Droplet.ID == 0 {
return nil, fmt.Errorf("Uninitialized droplet id for job %d", job.ID)
}
instanceToJobIds[job.Droplet.ID] = append(instanceToJobIds[job.Droplet.ID], job.ID)
}
return instanceToJobIds, nil
}

0 comments on commit 6353b73

Please sign in to comment.