diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..8bdfd3dd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +# Start by building the gflication. +FROM golang:1.17-bullseye as build + +WORKDIR /go/src/spotctl +ADD . /go/src/spotctl + +RUN go get -d -v ./... + +RUN go build -o /go/bin/spotctl + +# Now copy it into our base image. +FROM gcr.io/distroless/base-debian11 +COPY --from=build /go/bin/spotctl / +ENTRYPOINT ["/spotctl"] diff --git a/Makefile b/Makefile index e8e9a978..26d7b845 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,7 @@ todo: ## Show to-do items per file .PHONY: build build: fmt ## Build all commands $(Q) $(GO) build -trimpath -race -o dist/spotctl cmd/spotctl/main.go + cp dist/spotctl /home/pathcl/go/bin .PHONY: test test: fmt ## Run all tests diff --git a/internal/cmd/ocean/edit_cluster.go b/internal/cmd/ocean/edit_cluster.go index 6261a76f..244ffbc3 100644 --- a/internal/cmd/ocean/edit_cluster.go +++ b/internal/cmd/ocean/edit_cluster.go @@ -39,6 +39,7 @@ func newCmdEditCluster(opts *CmdEditOptions) *CmdEditCluster { func (x *CmdEditCluster) initSubCommands() { commands := []func(*CmdEditClusterOptions) *cobra.Command{ NewCmdEditClusterKubernetes, + NewCmdEditClusterECS, } for _, cmd := range commands { diff --git a/internal/cmd/ocean/edit_cluster_ecs.go b/internal/cmd/ocean/edit_cluster_ecs.go new file mode 100644 index 00000000..e1f4155d --- /dev/null +++ b/internal/cmd/ocean/edit_cluster_ecs.go @@ -0,0 +1,166 @@ +package ocean + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spotinst/spotctl/internal/errors" + "github.com/spotinst/spotctl/internal/flags" + "github.com/spotinst/spotctl/internal/spot" +) + +type ( + CmdEditClusterECS struct { + cmd *cobra.Command + opts CmdEditClusterECSOptions + } + + CmdEditClusterECSOptions struct { + *CmdEditClusterOptions + + ClusterID string + } +) + +func NewCmdEditClusterECS(opts *CmdEditClusterOptions) *cobra.Command { + return newCmdEditClusterECS(opts).cmd +} + +func newCmdEditClusterECS(opts *CmdEditClusterOptions) *CmdEditClusterECS { + var cmd CmdEditClusterECS + + cmd.cmd = &cobra.Command{ + Use: "ecs", + Short: "Edit a ECS cluster", + SilenceErrors: true, + SilenceUsage: true, + Aliases: []string{"e"}, + RunE: func(*cobra.Command, []string) error { + return cmd.Run(context.Background()) + }, + } + + cmd.opts.Init(cmd.cmd.Flags(), opts) + + return &cmd +} + +func (x *CmdEditClusterECS) Run(ctx context.Context) error { + steps := []func(context.Context) error{ + x.survey, + x.log, + x.validate, + x.run, + } + + for _, step := range steps { + if err := step(ctx); err != nil { + return err + } + } + + return nil +} + +func (x *CmdEditClusterECS) survey(ctx context.Context) error { + if x.opts.Noninteractive { + return nil + } + + return nil +} + +func (x *CmdEditClusterECS) log(ctx context.Context) error { + flags.Log(x.cmd) + return nil +} + +func (x *CmdEditClusterECS) validate(ctx context.Context) error { + return x.opts.Validate() +} + +func (x *CmdEditClusterECS) run(ctx context.Context) error { + spotClientOpts := []spot.ClientOption{ + spot.WithCredentialsProfile(x.opts.Profile), + spot.WithDryRun(x.opts.DryRun), + } + + spotClient, err := x.opts.Clientset.NewSpotClient(spotClientOpts...) + if err != nil { + return err + } + + oceanClient, err := spotClient.Services().Ocean(x.opts.CloudProvider, spot.OrchestratorECS) + if err != nil { + return err + } + + cluster, err := oceanClient.GetCluster(ctx, x.opts.ClusterID) + if err != nil { + return err + } + + rawJSON, err := json.MarshalIndent(cluster.Obj, "", " ") + if err != nil { + return err + } + + editor, err := x.opts.Clientset.NewEditor() + if err != nil { + return err + } + + editedJSON, path, err := editor.OpenTempFile(ctx, "spotinst", ".json", bytes.NewBuffer(rawJSON)) + if err != nil { + return err + } + + if bytes.Equal(rawJSON, editedJSON) { + os.Remove(path) + fmt.Fprintln(x.opts.Out, "Edit cancelled, no changes made.") + return nil + } + + if err := json.Unmarshal(editedJSON, cluster.Obj); err != nil { + return err + } + + _, err = oceanClient.UpdateCluster(ctx, cluster) + return err +} + +func (x *CmdEditClusterECSOptions) Init(fs *pflag.FlagSet, opts *CmdEditClusterOptions) { + x.initDefaults(opts) + x.initFlags(fs) +} + +func (x *CmdEditClusterECSOptions) initDefaults(opts *CmdEditClusterOptions) { + x.CmdEditClusterOptions = opts +} + +func (x *CmdEditClusterECSOptions) initFlags(fs *pflag.FlagSet) { + fs.StringVar(&x.ClusterID, flags.FlagOceanClusterID, x.ClusterID, "id of the cluster") +} + +func (x *CmdEditClusterECSOptions) Validate() error { + errg := errors.NewErrorGroup() + + if err := x.CmdEditClusterOptions.Validate(); err != nil { + errg.Add(err) + } + + if x.ClusterID == "" { + errg.Add(errors.Required("ClusterID")) + } + + if errg.Len() > 0 { + return errg + } + + return nil +} diff --git a/internal/cmd/ocean/get_cluster.go b/internal/cmd/ocean/get_cluster.go index 12a5f5c6..b728a2b9 100644 --- a/internal/cmd/ocean/get_cluster.go +++ b/internal/cmd/ocean/get_cluster.go @@ -39,6 +39,8 @@ func newCmdGetCluster(opts *CmdGetOptions) *CmdGetCluster { func (x *CmdGetCluster) initSubCommands() { commands := []func(*CmdGetClusterOptions) *cobra.Command{ NewCmdGetClusterKubernetes, + NewCmdGetClusterEcs, + NewCmdGetClusterLogs, } for _, cmd := range commands { diff --git a/internal/cmd/ocean/get_cluster_ecs.go b/internal/cmd/ocean/get_cluster_ecs.go new file mode 100644 index 00000000..b4c47c23 --- /dev/null +++ b/internal/cmd/ocean/get_cluster_ecs.go @@ -0,0 +1,119 @@ +package ocean + +import ( + "context" + "sort" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spotinst/spotctl/internal/flags" + "github.com/spotinst/spotctl/internal/spot" + "github.com/spotinst/spotctl/internal/writer" +) + +type ( + CmdGetClusterEcs struct { + cmd *cobra.Command + opts CmdGetClusterEcsOptions + } + + CmdGetClusterEcsOptions struct { + *CmdGetClusterOptions + } +) + +func NewCmdGetClusterEcs(opts *CmdGetClusterOptions) *cobra.Command { + return newCmdGetClusterEcs(opts).cmd +} + +func newCmdGetClusterEcs(opts *CmdGetClusterOptions) *CmdGetClusterEcs { + var cmd CmdGetClusterEcs + + cmd.cmd = &cobra.Command{ + Use: "ecs", + Short: "Display one or many ecs clusters", + SilenceErrors: true, + SilenceUsage: true, + Aliases: []string{"ecs"}, + RunE: func(*cobra.Command, []string) error { + return cmd.Run(context.Background()) + }, + } + + cmd.opts.Init(cmd.cmd.Flags(), opts) + + return &cmd +} + +func (x *CmdGetClusterEcs) Run(ctx context.Context) error { + steps := []func(context.Context) error{ + x.survey, + x.log, + x.validate, + x.run, + } + + for _, step := range steps { + if err := step(ctx); err != nil { + return err + } + } + + return nil +} + +func (x *CmdGetClusterEcs) survey(ctx context.Context) error { + if x.opts.Noninteractive { + return nil + } + + return nil +} + +func (x *CmdGetClusterEcs) log(ctx context.Context) error { + flags.Log(x.cmd) + return nil +} + +func (x *CmdGetClusterEcs) validate(ctx context.Context) error { + return x.opts.Validate() +} + +func (x *CmdGetClusterEcs) run(ctx context.Context) error { + spotClientOpts := []spot.ClientOption{ + spot.WithCredentialsProfile(x.opts.Profile), + spot.WithDryRun(x.opts.DryRun), + } + + spotClient, err := x.opts.Clientset.NewSpotClient(spotClientOpts...) + if err != nil { + return err + } + + oceanClient, err := spotClient.Services().Ocean(x.opts.CloudProvider, spot.OrchestratorECS) + if err != nil { + return err + } + + clusters, err := oceanClient.ListClusters(ctx) + if err != nil { + return err + } + + w, err := x.opts.Clientset.NewWriter(writer.Format(x.opts.Output)) + if err != nil { + return err + } + + sort.Sort(&spot.OceanClustersSorter{Clusters: clusters}) + + return w.Write(clusters) +} + +func (x *CmdGetClusterEcsOptions) Init(fs *pflag.FlagSet, opts *CmdGetClusterOptions) { + x.CmdGetClusterOptions = opts +} + +func (x *CmdGetClusterEcsOptions) Validate() error { + return x.CmdGetClusterOptions.Validate() +} diff --git a/internal/cmd/ocean/get_cluster_logs.go b/internal/cmd/ocean/get_cluster_logs.go new file mode 100644 index 00000000..845094d5 --- /dev/null +++ b/internal/cmd/ocean/get_cluster_logs.go @@ -0,0 +1,132 @@ +package ocean + +import ( + "context" + "fmt" + "log" + "os" + "strconv" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spotinst/spotctl/internal/flags" + "github.com/spotinst/spotinst-sdk-go/service/ocean" + "github.com/spotinst/spotinst-sdk-go/service/ocean/providers/aws" + "github.com/spotinst/spotinst-sdk-go/spotinst" + "github.com/spotinst/spotinst-sdk-go/spotinst/session" +) + +type ( + CmdGetClusterLogs struct { + cmd *cobra.Command + opts CmdGetClusterLogsOptions + } + + CmdGetClusterLogsOptions struct { + *CmdGetClusterOptions + } +) + +func NewCmdGetClusterLogs(opts *CmdGetClusterOptions) *cobra.Command { + return newCmdGetClusterLogs(opts).cmd +} + +func newCmdGetClusterLogs(opts *CmdGetClusterOptions) *CmdGetClusterLogs { + var cmd CmdGetClusterLogs + + cmd.cmd = &cobra.Command{ + Use: "logs", + Short: "Get Logs from ocean ecs", + SilenceErrors: true, + SilenceUsage: true, + Aliases: []string{"ecs"}, + RunE: func(*cobra.Command, []string) error { + return cmd.Run(context.Background()) + }, + } + + cmd.opts.Init(cmd.cmd.Flags(), opts) + + return &cmd +} + +func (x *CmdGetClusterLogs) Run(ctx context.Context) error { + steps := []func(context.Context) error{ + x.survey, + x.log, + x.validate, + x.run, + } + + for _, step := range steps { + if err := step(ctx); err != nil { + return err + } + } + + return nil +} + +func (x *CmdGetClusterLogs) survey(ctx context.Context) error { + if x.opts.Noninteractive { + return nil + } + + return nil +} + +func (x *CmdGetClusterLogs) log(ctx context.Context) error { + flags.Log(x.cmd) + return nil +} + +func (x *CmdGetClusterLogs) validate(ctx context.Context) error { + return x.opts.Validate() +} + +// get cluster logs + +func (x *CmdGetClusterLogs) run(ctx context.Context) error { + sess := session.New() + svc := ocean.New(sess) + + today := strconv.FormatInt(time.Now().Sub(time.Unix(0, 0)).Milliseconds(), 10) + lastWeek := strconv.FormatInt(time.Now().Sub(time.Unix(0, 0)).Milliseconds()-604800000, 10) + + t := time.Now().Unix() + timeT := time.Unix(t, 0) + fmt.Printf("From 1 week ago until: %s\n", timeT) + + // Get log events. + + cluster := os.Getenv("OCEAN_CLUSTER") + out, err := svc.CloudProviderAWS().GetLogEvents(ctx, &aws.GetLogEventsInput{ + ClusterID: spotinst.String(cluster), + FromDate: spotinst.String(lastWeek), + ToDate: spotinst.String(today), + }) + if err != nil { + log.Fatalf("spotinst: failed to get log events: %v", err) + } + + // Output log events, if any. + if len(out.Events) > 0 { + for _, event := range out.Events { + fmt.Printf("%s [%s] %s\n", + spotinst.TimeValue(event.CreatedAt).Format(time.RFC3339), + spotinst.StringValue(event.Severity), + spotinst.StringValue(event.Message)) + } + } + + return err +} + +func (x *CmdGetClusterLogsOptions) Init(fs *pflag.FlagSet, opts *CmdGetClusterOptions) { + x.CmdGetClusterOptions = opts +} + +func (x *CmdGetClusterLogsOptions) Validate() error { + return x.CmdGetClusterOptions.Validate() +}