Skip to content

feat: cli: add autostart and autostop commands #922

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

Merged
merged 2 commits into from
Apr 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions cli/workspaceautostart.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package cli

import (
"fmt"
"time"

"github.com/spf13/cobra"

"github.com/coder/coder/coderd/autostart/schedule"
"github.com/coder/coder/codersdk"
)

const autostartDescriptionLong = `To have your workspace build automatically at a regular time you can enable autostart.
When enabling autostart, provide a schedule. This schedule is in cron format except only
the following fields are allowed:
- minute
- hour
- day of week

For example, to start your workspace every weekday at 9.30 am, provide the schedule '30 9 1-5'.`

func workspaceAutostart() *cobra.Command {
autostartCmd := &cobra.Command{
Use: "autostart enable <workspace> <schedule>",
Short: "schedule a workspace to automatically start at a regular time",
Long: autostartDescriptionLong,
Example: "coder workspaces autostart enable my-workspace '30 9 1-5'",
Hidden: true, // TODO(cian): un-hide when autostart scheduling implemented
}

autostartCmd.AddCommand(workspaceAutostartEnable())
autostartCmd.AddCommand(workspaceAutostartDisable())

return autostartCmd
}

func workspaceAutostartEnable() *cobra.Command {
return &cobra.Command{
Use: "enable <workspace_name> <schedule>",
ValidArgsFunction: validArgsWorkspaceName,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}

workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0])
if err != nil {
return err
}

validSchedule, err := schedule.Weekly(args[1])
if err != nil {
return err
}

err = client.UpdateWorkspaceAutostart(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
Schedule: validSchedule.String(),
})
if err != nil {
return err
}

_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nThe %s workspace will automatically start at %s.\n\n", workspace.Name, validSchedule.Next(time.Now()))

return nil
},
}
}

func workspaceAutostartDisable() *cobra.Command {
return &cobra.Command{
Use: "disable <workspace_name>",
ValidArgsFunction: validArgsWorkspaceName,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}

workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0])
if err != nil {
return err
}

err = client.UpdateWorkspaceAutostart(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
Schedule: "",
})
if err != nil {
return err
}

_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nThe %s workspace will no longer automatically start.\n\n", workspace.Name)

return nil
},
}
}
150 changes: 150 additions & 0 deletions cli/workspaceautostart_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package cli_test

import (
"bytes"
"context"
"testing"

"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/stretchr/testify/require"
)

func TestWorkspaceAutostart(t *testing.T) {
t.Parallel()

t.Run("EnableDisableOK", func(t *testing.T) {
t.Parallel()

var (
ctx = context.Background()
client = coderdtest.New(t, nil)
_ = coderdtest.NewProvisionerDaemon(t, client)
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
sched = "CRON_TZ=Europe/Dublin 30 9 1-5"
stdoutBuf = &bytes.Buffer{}
)

cmd, root := clitest.New(t, "workspaces", "autostart", "enable", workspace.Name, sched)
clitest.SetupConfig(t, client, root)
cmd.SetOut(stdoutBuf)

err := cmd.Execute()
require.NoError(t, err, "unexpected error")
require.Contains(t, stdoutBuf.String(), "will automatically start at", "unexpected output")

// Ensure autostart schedule updated
updated, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch updated workspace")
require.Equal(t, sched, updated.AutostartSchedule, "expected autostart schedule to be set")

// Disable schedule
cmd, root = clitest.New(t, "workspaces", "autostart", "disable", workspace.Name)
clitest.SetupConfig(t, client, root)
cmd.SetOut(stdoutBuf)

err = cmd.Execute()
require.NoError(t, err, "unexpected error")
require.Contains(t, stdoutBuf.String(), "will no longer automatically start", "unexpected output")

// Ensure autostart schedule updated
updated, err = client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch updated workspace")
require.Empty(t, updated.AutostartSchedule, "expected autostart schedule to not be set")
})

t.Run("Enable_NotFound", func(t *testing.T) {
t.Parallel()

var (
client = coderdtest.New(t, nil)
_ = coderdtest.NewProvisionerDaemon(t, client)
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
sched = "CRON_TZ=Europe/Dublin 30 9 1-5"
)

cmd, root := clitest.New(t, "workspaces", "autostart", "enable", "doesnotexist", sched)
clitest.SetupConfig(t, client, root)

err := cmd.Execute()
require.ErrorContains(t, err, "status code 404: no workspace found by name", "unexpected error")
})

t.Run("Disable_NotFound", func(t *testing.T) {
t.Parallel()

var (
client = coderdtest.New(t, nil)
_ = coderdtest.NewProvisionerDaemon(t, client)
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
)

cmd, root := clitest.New(t, "workspaces", "autostart", "disable", "doesnotexist")
clitest.SetupConfig(t, client, root)

err := cmd.Execute()
require.ErrorContains(t, err, "status code 404: no workspace found by name", "unexpected error")
})

t.Run("Enable_InvalidSchedule", func(t *testing.T) {
t.Parallel()

var (
ctx = context.Background()
client = coderdtest.New(t, nil)
_ = coderdtest.NewProvisionerDaemon(t, client)
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
sched = "sdfasdfasdf asdf asdf"
)

cmd, root := clitest.New(t, "workspaces", "autostart", "enable", workspace.Name, sched)
clitest.SetupConfig(t, client, root)

err := cmd.Execute()
require.ErrorContains(t, err, "failed to parse int from sdfasdfasdf: strconv.Atoi:", "unexpected error")

// Ensure nothing happened
updated, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch updated workspace")
require.Empty(t, updated.AutostartSchedule, "expected autostart schedule to be empty")
})

t.Run("Enable_NoSchedule", func(t *testing.T) {
t.Parallel()

var (
ctx = context.Background()
client = coderdtest.New(t, nil)
_ = coderdtest.NewProvisionerDaemon(t, client)
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
)

cmd, root := clitest.New(t, "workspaces", "autostart", "enable", workspace.Name)
clitest.SetupConfig(t, client, root)

err := cmd.Execute()
require.ErrorContains(t, err, "accepts 2 arg(s), received 1", "unexpected error")

// Ensure nothing happened
updated, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch updated workspace")
require.Empty(t, updated.AutostartSchedule, "expected autostart schedule to be empty")
})
}
100 changes: 100 additions & 0 deletions cli/workspaceautostop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package cli

import (
"fmt"
"time"

"github.com/spf13/cobra"

"github.com/coder/coder/coderd/autostart/schedule"
"github.com/coder/coder/codersdk"
)

const autostopDescriptionLong = `To have your workspace stop automatically at a regular time you can enable autostop.
When enabling autostop, provide a schedule. This schedule is in cron format except only
the following fields are allowed:
- minute
- hour
- day of week

For example, to stop your workspace every weekday at 5.30 pm, provide the schedule '30 17 1-5'.`

func workspaceAutostop() *cobra.Command {
autostopCmd := &cobra.Command{
Use: "autostop enable <workspace> <schedule>",
Short: "schedule a workspace to automatically start at a regular time",
Long: autostopDescriptionLong,
Example: "coder workspaces autostop enable my-workspace '30 17 1-5'",
Hidden: true, // TODO(cian): un-hide when autostop scheduling implemented
}

autostopCmd.AddCommand(workspaceAutostopEnable())
autostopCmd.AddCommand(workspaceAutostopDisable())

return autostopCmd
}

func workspaceAutostopEnable() *cobra.Command {
return &cobra.Command{
Use: "enable <workspace_name> <schedule>",
ValidArgsFunction: validArgsWorkspaceName,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}

workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0])
if err != nil {
return err
}

validSchedule, err := schedule.Weekly(args[1])
if err != nil {
return err
}

err = client.UpdateWorkspaceAutostop(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceAutostopRequest{
Schedule: validSchedule.String(),
})
if err != nil {
return err
}

_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nThe %s workspace will automatically stop at %s.\n\n", workspace.Name, validSchedule.Next(time.Now()))

return nil
},
}
}

func workspaceAutostopDisable() *cobra.Command {
return &cobra.Command{
Use: "disable <workspace_name>",
ValidArgsFunction: validArgsWorkspaceName,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}

workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0])
if err != nil {
return err
}

err = client.UpdateWorkspaceAutostop(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceAutostopRequest{
Schedule: "",
})
if err != nil {
return err
}

_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nThe %s workspace will no longer automatically stop.\n\n", workspace.Name)

return nil
},
}
}
Loading