Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1 add size to status table #15

Merged
merged 14 commits into from
Oct 5, 2023
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 {
bohendo marked this conversation as resolved.
Show resolved Hide resolved
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)
bohendo marked this conversation as resolved.
Show resolved Hide resolved

// 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
}

bohendo marked this conversation as resolved.
Show resolved Hide resolved
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
}
2 changes: 1 addition & 1 deletion cmd/cloudexec/user_data.sh.tmpl
Original file line number Diff line number Diff line change
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
bohendo marked this conversation as resolved.
Show resolved Hide resolved
COMPLETED=true
# Remove the done flag temp file
rm "${exit_code_flag}"
break
Expand Down
52 changes: 52 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,38 @@ func CreateDroplet(config config.Config, username string, region string, size st
return droplet, nil
}

func GetDropletById(config config.Config, id int64) (Droplet, error) {
var droplet Droplet
// 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)
}

droplet = 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),
},
}
return droplet, nil
}
bohendo marked this conversation as resolved.
Show resolved Hide resolved

// 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 +274,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
6 changes: 3 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"
bohendo marked this conversation as resolved.
Show resolved Hide resolved
"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"`
bohendo marked this conversation as resolved.
Show resolved Hide resolved
}

type State struct {
Expand Down Expand Up @@ -210,7 +210,7 @@ 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)
instanceToJobIds[job.Droplet.ID] = append(instanceToJobIds[job.Droplet.ID], job.ID)
bohendo marked this conversation as resolved.
Show resolved Hide resolved
}
return instanceToJobIds, nil
}