From 5142bcf6a61295ff168b99f4b18faf6c17ef786d Mon Sep 17 00:00:00 2001 From: Pascal Breuninger Date: Wed, 5 Jun 2024 08:50:58 +0200 Subject: [PATCH] feat(cli): Add sleep mode to pro instances CLI --- cmd/pro/pro.go | 2 + cmd/pro/rebuild.go | 19 ++++++- cmd/pro/sleep.go | 127 +++++++++++++++++++++++++++++++++++++++++++ cmd/pro/wakeup.go | 131 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 cmd/pro/sleep.go create mode 100644 cmd/pro/wakeup.go diff --git a/cmd/pro/pro.go b/cmd/pro/pro.go index 406cbfd97..f3305500b 100644 --- a/cmd/pro/pro.go +++ b/cmd/pro/pro.go @@ -47,6 +47,8 @@ func NewProCmd(flags *flags.GlobalFlags, streamLogger *log.StreamLogger) *cobra. proCmd.AddCommand(NewImportCmd(globalFlags)) proCmd.AddCommand(NewStartCmd(globalFlags)) proCmd.AddCommand(NewRebuildCmd(globalFlags)) + proCmd.AddCommand(NewSleepCmd(globalFlags)) + proCmd.AddCommand(NewWakeupCmd(globalFlags)) proCmd.AddCommand(reset.NewResetCmd(globalFlags)) proCmd.AddCommand(provider.NewProProviderCmd(globalFlags)) return proCmd diff --git a/cmd/pro/rebuild.go b/cmd/pro/rebuild.go index 25203d23c..7da280876 100644 --- a/cmd/pro/rebuild.go +++ b/cmd/pro/rebuild.go @@ -9,6 +9,7 @@ import ( "github.com/loft-sh/devpod/cmd/pro/flags" "github.com/loft-sh/devpod/cmd/pro/provider" + "github.com/loft-sh/devpod/pkg/config" "github.com/loft-sh/devpod/pkg/loft/client" "github.com/loft-sh/devpod/pkg/loft/remotecommand" "github.com/loft-sh/log" @@ -23,6 +24,7 @@ type RebuildCmd struct { Log log.Logger Project string + Host string } // NewRebuildCmd creates a new command @@ -43,6 +45,8 @@ func NewRebuildCmd(globalFlags *flags.GlobalFlags) *cobra.Command { c.Flags().StringVar(&cmd.Project, "project", "", "The project to use") _ = c.MarkFlagRequired("project") + c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") + _ = c.MarkFlagRequired("host") return c } @@ -53,7 +57,20 @@ func (cmd *RebuildCmd) Run(ctx context.Context, args []string) error { } targetWorkspace := args[0] - baseClient, err := client.InitClientFromPath(ctx, cmd.Config) + devPodConfig, err := config.LoadConfig(cmd.Context, "") + if err != nil { + return err + } + providerConfig, err := resolveProInstance(devPodConfig, cmd.Host, cmd.Log) + if err != nil { + return fmt.Errorf("resolve host \"%s\": %w", cmd.Host, err) + } + configPath, err := LoftConfigPath(devPodConfig, providerConfig.Name) + if err != nil { + return fmt.Errorf("loft config path: %w", err) + } + + baseClient, err := client.InitClientFromPath(ctx, configPath) if err != nil { return err } diff --git a/cmd/pro/sleep.go b/cmd/pro/sleep.go new file mode 100644 index 000000000..27f8f8691 --- /dev/null +++ b/cmd/pro/sleep.go @@ -0,0 +1,127 @@ +package pro + +import ( + "context" + "fmt" + "strconv" + "time" + + clusterv1 "github.com/loft-sh/agentapi/v4/pkg/apis/loft/cluster/v1" + storagev1 "github.com/loft-sh/api/v4/pkg/apis/storage/v1" + "github.com/loft-sh/devpod/cmd/pro/flags" + "github.com/loft-sh/devpod/cmd/pro/provider" + "github.com/loft-sh/devpod/pkg/config" + "github.com/loft-sh/devpod/pkg/loft" + "github.com/loft-sh/devpod/pkg/loft/client" + "github.com/loft-sh/devpod/pkg/loft/project" + "github.com/loft-sh/log" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +// SleepCmd holds the cmd flags +type SleepCmd struct { + *flags.GlobalFlags + Log log.Logger + + Project string + Host string + ForceDuration int64 +} + +// NewSleepCmd creates a new command +func NewSleepCmd(globalFlags *flags.GlobalFlags) *cobra.Command { + cmd := &SleepCmd{ + GlobalFlags: globalFlags, + Log: log.GetInstance(), + } + c := &cobra.Command{ + Use: "sleep", + Short: "Put a workspace to sleep", + RunE: func(cobraCmd *cobra.Command, args []string) error { + log.Default.SetFormat(log.TextFormat) + + return cmd.Run(cobraCmd.Context(), args) + }, + } + + c.Flags().StringVar(&cmd.Project, "project", "", "The project to use") + c.Flags().Int64Var(&cmd.ForceDuration, "prevent-wakeup", -1, "The amount of seconds this workspace should sleep until it can be woken up again (use 0 for infinite sleeping). During this time the space can only be woken up by `devpod pro wakeup`, manually deleting the annotation on the namespace or through the UI") + _ = c.MarkFlagRequired("project") + c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") + _ = c.MarkFlagRequired("host") + + return c +} + +func (cmd *SleepCmd) Run(ctx context.Context, args []string) error { + if len(args) == 0 { + return fmt.Errorf("please provide a workspace name") + } + targetWorkspace := args[0] + + devPodConfig, err := config.LoadConfig(cmd.Context, "") + if err != nil { + return err + } + providerConfig, err := resolveProInstance(devPodConfig, cmd.Host, cmd.Log) + if err != nil { + return fmt.Errorf("resolve host \"%s\": %w", cmd.Host, err) + } + configPath, err := LoftConfigPath(devPodConfig, providerConfig.Name) + if err != nil { + return fmt.Errorf("loft config path: %w", err) + } + + baseClient, err := client.InitClientFromPath(ctx, configPath) + if err != nil { + return err + } + + workspaceInstance, err := provider.FindWorkspaceByName(ctx, baseClient, targetWorkspace, cmd.Project) + if err != nil { + return err + } + + managementClient, err := baseClient.Management() + if err != nil { + return err + } + + patch := ctrlclient.MergeFrom(workspaceInstance.DeepCopy()) + if workspaceInstance.Annotations == nil { + workspaceInstance.Annotations = map[string]string{} + } + workspaceInstance.Annotations[clusterv1.SleepModeForceAnnotation] = "true" + if cmd.ForceDuration >= 0 { + workspaceInstance.Annotations[clusterv1.SleepModeForceDurationAnnotation] = strconv.FormatInt(cmd.ForceDuration, 10) + } + patchData, err := patch.Data(workspaceInstance) + if err != nil { + return fmt.Errorf("create patch: %w", err) + } + + _, err = managementClient.Loft().ManagementV1().DevPodWorkspaceInstances(project.ProjectNamespace(cmd.Project)).Patch(ctx, workspaceInstance.Name, patch.Type(), patchData, metav1.PatchOptions{}) + if err != nil { + return err + } + + // wait for sleeping + cmd.Log.Info("Wait until workspace is sleeping...") + err = wait.PollUntilContextTimeout(ctx, time.Second, loft.Timeout(), false, func(ctx context.Context) (done bool, err error) { + workspaceInstance, err := managementClient.Loft().ManagementV1().DevPodWorkspaceInstances(project.ProjectNamespace(cmd.Project)).Get(ctx, workspaceInstance.Name, metav1.GetOptions{}) + if err != nil { + return false, err + } + + return workspaceInstance.Status.Phase == storagev1.InstanceSleeping, nil + }) + if err != nil { + return fmt.Errorf("error waiting for workspace to start sleeping: %w", err) + } + + cmd.Log.Donef("Successfully put workspace %s to sleep", workspaceInstance.Name) + return nil +} diff --git a/cmd/pro/wakeup.go b/cmd/pro/wakeup.go new file mode 100644 index 000000000..955ae52a9 --- /dev/null +++ b/cmd/pro/wakeup.go @@ -0,0 +1,131 @@ +package pro + +import ( + "context" + "fmt" + "strconv" + "time" + + clusterv1 "github.com/loft-sh/agentapi/v4/pkg/apis/loft/cluster/v1" + storagev1 "github.com/loft-sh/api/v4/pkg/apis/storage/v1" + "github.com/loft-sh/devpod/cmd/pro/flags" + "github.com/loft-sh/devpod/cmd/pro/provider" + "github.com/loft-sh/devpod/pkg/config" + "github.com/loft-sh/devpod/pkg/loft" + "github.com/loft-sh/devpod/pkg/loft/client" + "github.com/loft-sh/devpod/pkg/loft/project" + "github.com/loft-sh/log" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +// WakeupCmd holds the cmd flags +type WakeupCmd struct { + *flags.GlobalFlags + Log log.Logger + + Project string + Host string +} + +// NewWakeupCmd creates a new command +func NewWakeupCmd(globalFlags *flags.GlobalFlags) *cobra.Command { + cmd := &WakeupCmd{ + GlobalFlags: globalFlags, + Log: log.GetInstance(), + } + c := &cobra.Command{ + Use: "wakeup", + Short: "Wake a workspace up", + RunE: func(cobraCmd *cobra.Command, args []string) error { + log.Default.SetFormat(log.TextFormat) + + return cmd.Run(cobraCmd.Context(), args) + }, + } + + c.Flags().StringVar(&cmd.Project, "project", "", "The project to use") + _ = c.MarkFlagRequired("project") + c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") + _ = c.MarkFlagRequired("host") + + return c +} + +func (cmd *WakeupCmd) Run(ctx context.Context, args []string) error { + if len(args) == 0 { + return fmt.Errorf("please provide a workspace name") + } + targetWorkspace := args[0] + + devPodConfig, err := config.LoadConfig(cmd.Context, "") + if err != nil { + return err + } + providerConfig, err := resolveProInstance(devPodConfig, cmd.Host, cmd.Log) + if err != nil { + return fmt.Errorf("resolve host \"%s\": %w", cmd.Host, err) + } + configPath, err := LoftConfigPath(devPodConfig, providerConfig.Name) + if err != nil { + return fmt.Errorf("loft config path: %w", err) + } + + baseClient, err := client.InitClientFromPath(ctx, configPath) + if err != nil { + return err + } + + workspaceInstance, err := provider.FindWorkspaceByName(ctx, baseClient, targetWorkspace, cmd.Project) + if err != nil { + return err + } + + if workspaceInstance.Status.Phase != storagev1.InstanceSleeping { + cmd.Log.Infof("Workspace %s is not sleeping", targetWorkspace, workspaceInstance.Name) + return nil + } + + managementClient, err := baseClient.Management() + if err != nil { + return err + } + + patch := ctrlclient.MergeFrom(workspaceInstance.DeepCopy()) + + if workspaceInstance.Annotations == nil { + workspaceInstance.Annotations = map[string]string{} + } + delete(workspaceInstance.Annotations, clusterv1.SleepModeForceAnnotation) + delete(workspaceInstance.Annotations, clusterv1.SleepModeForceDurationAnnotation) + workspaceInstance.Annotations[clusterv1.SleepModeLastActivityAnnotation] = strconv.FormatInt(time.Now().Unix(), 10) + + patchData, err := patch.Data(workspaceInstance) + if err != nil { + return err + } + + _, err = managementClient.Loft().ManagementV1().DevPodWorkspaceInstances(project.ProjectNamespace(cmd.Project)).Patch(ctx, workspaceInstance.Name, patch.Type(), patchData, metav1.PatchOptions{}) + if err != nil { + return err + } + + // wait for sleeping + cmd.Log.Info("Wait until workspace wakes up...") + err = wait.PollUntilContextTimeout(ctx, time.Second, loft.Timeout(), false, func(ctx context.Context) (done bool, err error) { + workspaceInstance, err := managementClient.Loft().ManagementV1().DevPodWorkspaceInstances(project.ProjectNamespace(cmd.Project)).Get(ctx, workspaceInstance.Name, metav1.GetOptions{}) + if err != nil { + return false, err + } + + return workspaceInstance.Status.Phase == storagev1.InstanceReady, nil + }) + if err != nil { + return fmt.Errorf("error waiting for workspace to wake up: %w", err) + } + + cmd.Log.Donef("Successfully woke up workspace %s", workspaceInstance.Name) + return nil +}