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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy