Skip to content

Commit 53db178

Browse files
authored
feat: cli: add autostart and autostop commands (#922)
* feat: cli: add autostart and autostop commands * fix: autostart/autostop: add help and usage, hide for now
1 parent cb5b228 commit 53db178

File tree

5 files changed

+502
-0
lines changed

5 files changed

+502
-0
lines changed

cli/workspaceautostart.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/spf13/cobra"
8+
9+
"github.com/coder/coder/coderd/autostart/schedule"
10+
"github.com/coder/coder/codersdk"
11+
)
12+
13+
const autostartDescriptionLong = `To have your workspace build automatically at a regular time you can enable autostart.
14+
When enabling autostart, provide a schedule. This schedule is in cron format except only
15+
the following fields are allowed:
16+
- minute
17+
- hour
18+
- day of week
19+
20+
For example, to start your workspace every weekday at 9.30 am, provide the schedule '30 9 1-5'.`
21+
22+
func workspaceAutostart() *cobra.Command {
23+
autostartCmd := &cobra.Command{
24+
Use: "autostart enable <workspace> <schedule>",
25+
Short: "schedule a workspace to automatically start at a regular time",
26+
Long: autostartDescriptionLong,
27+
Example: "coder workspaces autostart enable my-workspace '30 9 1-5'",
28+
Hidden: true, // TODO(cian): un-hide when autostart scheduling implemented
29+
}
30+
31+
autostartCmd.AddCommand(workspaceAutostartEnable())
32+
autostartCmd.AddCommand(workspaceAutostartDisable())
33+
34+
return autostartCmd
35+
}
36+
37+
func workspaceAutostartEnable() *cobra.Command {
38+
return &cobra.Command{
39+
Use: "enable <workspace_name> <schedule>",
40+
ValidArgsFunction: validArgsWorkspaceName,
41+
Args: cobra.ExactArgs(2),
42+
RunE: func(cmd *cobra.Command, args []string) error {
43+
client, err := createClient(cmd)
44+
if err != nil {
45+
return err
46+
}
47+
48+
workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0])
49+
if err != nil {
50+
return err
51+
}
52+
53+
validSchedule, err := schedule.Weekly(args[1])
54+
if err != nil {
55+
return err
56+
}
57+
58+
err = client.UpdateWorkspaceAutostart(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
59+
Schedule: validSchedule.String(),
60+
})
61+
if err != nil {
62+
return err
63+
}
64+
65+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nThe %s workspace will automatically start at %s.\n\n", workspace.Name, validSchedule.Next(time.Now()))
66+
67+
return nil
68+
},
69+
}
70+
}
71+
72+
func workspaceAutostartDisable() *cobra.Command {
73+
return &cobra.Command{
74+
Use: "disable <workspace_name>",
75+
ValidArgsFunction: validArgsWorkspaceName,
76+
Args: cobra.ExactArgs(1),
77+
RunE: func(cmd *cobra.Command, args []string) error {
78+
client, err := createClient(cmd)
79+
if err != nil {
80+
return err
81+
}
82+
83+
workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0])
84+
if err != nil {
85+
return err
86+
}
87+
88+
err = client.UpdateWorkspaceAutostart(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
89+
Schedule: "",
90+
})
91+
if err != nil {
92+
return err
93+
}
94+
95+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nThe %s workspace will no longer automatically start.\n\n", workspace.Name)
96+
97+
return nil
98+
},
99+
}
100+
}

cli/workspaceautostart_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package cli_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"testing"
7+
8+
"github.com/coder/coder/cli/clitest"
9+
"github.com/coder/coder/coderd/coderdtest"
10+
"github.com/coder/coder/codersdk"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestWorkspaceAutostart(t *testing.T) {
15+
t.Parallel()
16+
17+
t.Run("EnableDisableOK", func(t *testing.T) {
18+
t.Parallel()
19+
20+
var (
21+
ctx = context.Background()
22+
client = coderdtest.New(t, nil)
23+
_ = coderdtest.NewProvisionerDaemon(t, client)
24+
user = coderdtest.CreateFirstUser(t, client)
25+
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
26+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
27+
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
28+
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
29+
sched = "CRON_TZ=Europe/Dublin 30 9 1-5"
30+
stdoutBuf = &bytes.Buffer{}
31+
)
32+
33+
cmd, root := clitest.New(t, "workspaces", "autostart", "enable", workspace.Name, sched)
34+
clitest.SetupConfig(t, client, root)
35+
cmd.SetOut(stdoutBuf)
36+
37+
err := cmd.Execute()
38+
require.NoError(t, err, "unexpected error")
39+
require.Contains(t, stdoutBuf.String(), "will automatically start at", "unexpected output")
40+
41+
// Ensure autostart schedule updated
42+
updated, err := client.Workspace(ctx, workspace.ID)
43+
require.NoError(t, err, "fetch updated workspace")
44+
require.Equal(t, sched, updated.AutostartSchedule, "expected autostart schedule to be set")
45+
46+
// Disable schedule
47+
cmd, root = clitest.New(t, "workspaces", "autostart", "disable", workspace.Name)
48+
clitest.SetupConfig(t, client, root)
49+
cmd.SetOut(stdoutBuf)
50+
51+
err = cmd.Execute()
52+
require.NoError(t, err, "unexpected error")
53+
require.Contains(t, stdoutBuf.String(), "will no longer automatically start", "unexpected output")
54+
55+
// Ensure autostart schedule updated
56+
updated, err = client.Workspace(ctx, workspace.ID)
57+
require.NoError(t, err, "fetch updated workspace")
58+
require.Empty(t, updated.AutostartSchedule, "expected autostart schedule to not be set")
59+
})
60+
61+
t.Run("Enable_NotFound", func(t *testing.T) {
62+
t.Parallel()
63+
64+
var (
65+
client = coderdtest.New(t, nil)
66+
_ = coderdtest.NewProvisionerDaemon(t, client)
67+
user = coderdtest.CreateFirstUser(t, client)
68+
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
69+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
70+
sched = "CRON_TZ=Europe/Dublin 30 9 1-5"
71+
)
72+
73+
cmd, root := clitest.New(t, "workspaces", "autostart", "enable", "doesnotexist", sched)
74+
clitest.SetupConfig(t, client, root)
75+
76+
err := cmd.Execute()
77+
require.ErrorContains(t, err, "status code 404: no workspace found by name", "unexpected error")
78+
})
79+
80+
t.Run("Disable_NotFound", func(t *testing.T) {
81+
t.Parallel()
82+
83+
var (
84+
client = coderdtest.New(t, nil)
85+
_ = coderdtest.NewProvisionerDaemon(t, client)
86+
user = coderdtest.CreateFirstUser(t, client)
87+
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
88+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
89+
)
90+
91+
cmd, root := clitest.New(t, "workspaces", "autostart", "disable", "doesnotexist")
92+
clitest.SetupConfig(t, client, root)
93+
94+
err := cmd.Execute()
95+
require.ErrorContains(t, err, "status code 404: no workspace found by name", "unexpected error")
96+
})
97+
98+
t.Run("Enable_InvalidSchedule", func(t *testing.T) {
99+
t.Parallel()
100+
101+
var (
102+
ctx = context.Background()
103+
client = coderdtest.New(t, nil)
104+
_ = coderdtest.NewProvisionerDaemon(t, client)
105+
user = coderdtest.CreateFirstUser(t, client)
106+
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
107+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
108+
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
109+
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
110+
sched = "sdfasdfasdf asdf asdf"
111+
)
112+
113+
cmd, root := clitest.New(t, "workspaces", "autostart", "enable", workspace.Name, sched)
114+
clitest.SetupConfig(t, client, root)
115+
116+
err := cmd.Execute()
117+
require.ErrorContains(t, err, "failed to parse int from sdfasdfasdf: strconv.Atoi:", "unexpected error")
118+
119+
// Ensure nothing happened
120+
updated, err := client.Workspace(ctx, workspace.ID)
121+
require.NoError(t, err, "fetch updated workspace")
122+
require.Empty(t, updated.AutostartSchedule, "expected autostart schedule to be empty")
123+
})
124+
125+
t.Run("Enable_NoSchedule", func(t *testing.T) {
126+
t.Parallel()
127+
128+
var (
129+
ctx = context.Background()
130+
client = coderdtest.New(t, nil)
131+
_ = coderdtest.NewProvisionerDaemon(t, client)
132+
user = coderdtest.CreateFirstUser(t, client)
133+
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
134+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
135+
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
136+
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
137+
)
138+
139+
cmd, root := clitest.New(t, "workspaces", "autostart", "enable", workspace.Name)
140+
clitest.SetupConfig(t, client, root)
141+
142+
err := cmd.Execute()
143+
require.ErrorContains(t, err, "accepts 2 arg(s), received 1", "unexpected error")
144+
145+
// Ensure nothing happened
146+
updated, err := client.Workspace(ctx, workspace.ID)
147+
require.NoError(t, err, "fetch updated workspace")
148+
require.Empty(t, updated.AutostartSchedule, "expected autostart schedule to be empty")
149+
})
150+
}

cli/workspaceautostop.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/spf13/cobra"
8+
9+
"github.com/coder/coder/coderd/autostart/schedule"
10+
"github.com/coder/coder/codersdk"
11+
)
12+
13+
const autostopDescriptionLong = `To have your workspace stop automatically at a regular time you can enable autostop.
14+
When enabling autostop, provide a schedule. This schedule is in cron format except only
15+
the following fields are allowed:
16+
- minute
17+
- hour
18+
- day of week
19+
20+
For example, to stop your workspace every weekday at 5.30 pm, provide the schedule '30 17 1-5'.`
21+
22+
func workspaceAutostop() *cobra.Command {
23+
autostopCmd := &cobra.Command{
24+
Use: "autostop enable <workspace> <schedule>",
25+
Short: "schedule a workspace to automatically start at a regular time",
26+
Long: autostopDescriptionLong,
27+
Example: "coder workspaces autostop enable my-workspace '30 17 1-5'",
28+
Hidden: true, // TODO(cian): un-hide when autostop scheduling implemented
29+
}
30+
31+
autostopCmd.AddCommand(workspaceAutostopEnable())
32+
autostopCmd.AddCommand(workspaceAutostopDisable())
33+
34+
return autostopCmd
35+
}
36+
37+
func workspaceAutostopEnable() *cobra.Command {
38+
return &cobra.Command{
39+
Use: "enable <workspace_name> <schedule>",
40+
ValidArgsFunction: validArgsWorkspaceName,
41+
Args: cobra.ExactArgs(2),
42+
RunE: func(cmd *cobra.Command, args []string) error {
43+
client, err := createClient(cmd)
44+
if err != nil {
45+
return err
46+
}
47+
48+
workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0])
49+
if err != nil {
50+
return err
51+
}
52+
53+
validSchedule, err := schedule.Weekly(args[1])
54+
if err != nil {
55+
return err
56+
}
57+
58+
err = client.UpdateWorkspaceAutostop(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceAutostopRequest{
59+
Schedule: validSchedule.String(),
60+
})
61+
if err != nil {
62+
return err
63+
}
64+
65+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nThe %s workspace will automatically stop at %s.\n\n", workspace.Name, validSchedule.Next(time.Now()))
66+
67+
return nil
68+
},
69+
}
70+
}
71+
72+
func workspaceAutostopDisable() *cobra.Command {
73+
return &cobra.Command{
74+
Use: "disable <workspace_name>",
75+
ValidArgsFunction: validArgsWorkspaceName,
76+
Args: cobra.ExactArgs(1),
77+
RunE: func(cmd *cobra.Command, args []string) error {
78+
client, err := createClient(cmd)
79+
if err != nil {
80+
return err
81+
}
82+
83+
workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0])
84+
if err != nil {
85+
return err
86+
}
87+
88+
err = client.UpdateWorkspaceAutostop(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceAutostopRequest{
89+
Schedule: "",
90+
})
91+
if err != nil {
92+
return err
93+
}
94+
95+
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nThe %s workspace will no longer automatically stop.\n\n", workspace.Name)
96+
97+
return nil
98+
},
99+
}
100+
}

0 commit comments

Comments
 (0)