Skip to content

Commit

Permalink
feat(cli): Add sleep mode to pro instances CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
pascalbreuninger committed Jun 7, 2024
1 parent e1633a8 commit 5142bcf
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 1 deletion.
2 changes: 2 additions & 0 deletions cmd/pro/pro.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion cmd/pro/rebuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -23,6 +24,7 @@ type RebuildCmd struct {
Log log.Logger

Project string
Host string
}

// NewRebuildCmd creates a new command
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down
127 changes: 127 additions & 0 deletions cmd/pro/sleep.go
Original file line number Diff line number Diff line change
@@ -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
}
131 changes: 131 additions & 0 deletions cmd/pro/wakeup.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 5142bcf

Please sign in to comment.