From a072ff89e6284d8b7cd5edbb2535e0095722c671 Mon Sep 17 00:00:00 2001
From: johnstcn <public@cianjohnston.ie>
Date: Fri, 17 Jun 2022 17:33:43 +0000
Subject: [PATCH] fix: cli: create: use new autostart format, opt-in by default

---
 cli/create.go      |  65 ++++++------------------
 cli/create_test.go | 120 +++++++--------------------------------------
 2 files changed, 33 insertions(+), 152 deletions(-)

diff --git a/cli/create.go b/cli/create.go
index 1a0c89cc62a27..258e272c01dbf 100644
--- a/cli/create.go
+++ b/cli/create.go
@@ -10,21 +10,17 @@ import (
 
 	"github.com/coder/coder/cli/cliflag"
 	"github.com/coder/coder/cli/cliui"
-	"github.com/coder/coder/coderd/autobuild/schedule"
 	"github.com/coder/coder/coderd/util/ptr"
 	"github.com/coder/coder/codersdk"
 )
 
 func create() *cobra.Command {
 	var (
-		autostartMinute string
-		autostartHour   string
-		autostartDow    string
-		parameterFile   string
-		templateName    string
-		ttl             time.Duration
-		tzName          string
-		workspaceName   string
+		parameterFile string
+		templateName  string
+		startAt       string
+		stopAfter     time.Duration
+		workspaceName string
 	)
 	cmd := &cobra.Command{
 		Annotations: workspaceCommand,
@@ -115,21 +111,13 @@ func create() *cobra.Command {
 				}
 			}
 
-			schedSpec, err := validSchedule(
-				autostartMinute,
-				autostartHour,
-				autostartDow,
-				tzName,
-				time.Duration(template.MinAutostartIntervalMillis)*time.Millisecond,
-			)
-			if err != nil {
-				return xerrors.Errorf("Invalid autostart schedule: %w", err)
-			}
-			if ttl < time.Minute {
-				return xerrors.Errorf("TTL must be at least 1 minute")
-			}
-			if ttlMax := time.Duration(template.MaxTTLMillis) * time.Millisecond; ttl > ttlMax {
-				return xerrors.Errorf("TTL must be below template maximum %s", ttlMax)
+			var schedSpec *string
+			if startAt != "" {
+				sched, err := parseCLISchedule(startAt)
+				if err != nil {
+					return err
+				}
+				schedSpec = ptr.Ref(sched.String())
 			}
 
 			templateVersion, err := client.TemplateVersion(cmd.Context(), template.ActiveVersionID)
@@ -230,7 +218,7 @@ func create() *cobra.Command {
 				TemplateID:        template.ID,
 				Name:              workspaceName,
 				AutostartSchedule: schedSpec,
-				TTLMillis:         ptr.Ref(ttl.Milliseconds()),
+				TTLMillis:         ptr.Ref(stopAfter.Milliseconds()),
 				ParameterValues:   parameters,
 			})
 			if err != nil {
@@ -250,30 +238,7 @@ func create() *cobra.Command {
 	cliui.AllowSkipPrompt(cmd)
 	cliflag.StringVarP(cmd.Flags(), &templateName, "template", "t", "CODER_TEMPLATE_NAME", "", "Specify a template name.")
 	cliflag.StringVarP(cmd.Flags(), &parameterFile, "parameter-file", "", "CODER_PARAMETER_FILE", "", "Specify a file path with parameter values.")
-	cliflag.StringVarP(cmd.Flags(), &autostartMinute, "autostart-minute", "", "CODER_WORKSPACE_AUTOSTART_MINUTE", "0", "Specify the minute(s) at which the workspace should autostart (e.g. 0).")
-	cliflag.StringVarP(cmd.Flags(), &autostartHour, "autostart-hour", "", "CODER_WORKSPACE_AUTOSTART_HOUR", "9", "Specify the hour(s) at which the workspace should autostart (e.g. 9).")
-	cliflag.StringVarP(cmd.Flags(), &autostartDow, "autostart-day-of-week", "", "CODER_WORKSPACE_AUTOSTART_DOW", "MON-FRI", "Specify the days(s) on which the workspace should autostart (e.g. MON,TUE,WED,THU,FRI)")
-	cliflag.StringVarP(cmd.Flags(), &tzName, "tz", "", "TZ", "UTC", "Specify your timezone location for workspace autostart (e.g. US/Central).")
-	cliflag.DurationVarP(cmd.Flags(), &ttl, "ttl", "", "CODER_WORKSPACE_TTL", 8*time.Hour, "Specify a time-to-live (TTL) for the workspace (e.g. 8h).")
+	cliflag.StringVarP(cmd.Flags(), &startAt, "start-at", "", "CODER_WORKSPACE_START_AT", "", "Specify the workspace autostart schedule. Check `coder schedule start --help` for the syntax.")
+	cliflag.DurationVarP(cmd.Flags(), &stopAfter, "stop-after", "", "CODER_WORKSPACE_STOP_AFTER", 8*time.Hour, "Specify a duration after which the workspace should shut down (e.g. 8h).")
 	return cmd
 }
-
-func validSchedule(minute, hour, dow, tzName string, min time.Duration) (*string, error) {
-	_, err := time.LoadLocation(tzName)
-	if err != nil {
-		return nil, xerrors.Errorf("Invalid workspace autostart timezone: %w", err)
-	}
-
-	schedSpec := fmt.Sprintf("CRON_TZ=%s %s %s * * %s", tzName, minute, hour, dow)
-
-	sched, err := schedule.Weekly(schedSpec)
-	if err != nil {
-		return nil, err
-	}
-
-	if schedMin := sched.Min(); schedMin < min {
-		return nil, xerrors.Errorf("minimum autostart interval %s is above template constraint %s", schedMin, min)
-	}
-
-	return &schedSpec, nil
-}
diff --git a/cli/create_test.go b/cli/create_test.go
index 63654df7378b6..cd069706ea71e 100644
--- a/cli/create_test.go
+++ b/cli/create_test.go
@@ -14,7 +14,6 @@ import (
 	"github.com/coder/coder/cli/clitest"
 	"github.com/coder/coder/coderd/coderdtest"
 	"github.com/coder/coder/coderd/database"
-	"github.com/coder/coder/coderd/util/ptr"
 	"github.com/coder/coder/codersdk"
 	"github.com/coder/coder/provisioner/echo"
 	"github.com/coder/coder/provisionersdk/proto"
@@ -38,11 +37,8 @@ func TestCreate(t *testing.T) {
 			"create",
 			"my-workspace",
 			"--template", template.Name,
-			"--tz", "US/Central",
-			"--autostart-minute", "0",
-			"--autostart-hour", "*/2",
-			"--autostart-day-of-week", "MON-FRI",
-			"--ttl", "8h",
+			"--start-at", "9:30AM Mon-Fri US/Central",
+			"--stop-after", "8h",
 		}
 		cmd, root := clitest.New(t, args...)
 		clitest.SetupConfig(t, client, root)
@@ -70,103 +66,17 @@ func TestCreate(t *testing.T) {
 			}
 		}
 		<-doneChan
-	})
-
-	t.Run("AboveTemplateMaxTTL", func(t *testing.T) {
-		t.Parallel()
-		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
-		user := coderdtest.CreateFirstUser(t, client)
-		version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
-		coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
-		template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
-			ctr.MaxTTLMillis = ptr.Ref((12 * time.Hour).Milliseconds())
-		})
-		args := []string{
-			"create",
-			"my-workspace",
-			"--template", template.Name,
-			"--ttl", "12h1m",
-			"-y", // don't bother with waiting
-		}
-		cmd, root := clitest.New(t, args...)
-		clitest.SetupConfig(t, client, root)
-		pty := ptytest.New(t)
-		cmd.SetIn(pty.Input())
-		cmd.SetOut(pty.Output())
-		err := cmd.Execute()
-		assert.ErrorContains(t, err, "TTL must be below template maximum 12h0m0s")
-	})
-
-	t.Run("BelowTemplateMinAutostartInterval", func(t *testing.T) {
-		t.Parallel()
-		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
-		user := coderdtest.CreateFirstUser(t, client)
-		version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
-		coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
-		template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
-			ctr.MinAutostartIntervalMillis = ptr.Ref(time.Hour.Milliseconds())
-		})
-		args := []string{
-			"create",
-			"my-workspace",
-			"--template", template.Name,
-			"--autostart-minute", "*", // Every minute
-			"--autostart-hour", "*", // Every hour
-			"-y", // don't bother with waiting
-		}
-		cmd, root := clitest.New(t, args...)
-		clitest.SetupConfig(t, client, root)
-		pty := ptytest.New(t)
-		cmd.SetIn(pty.Input())
-		cmd.SetOut(pty.Output())
-		err := cmd.Execute()
-		assert.ErrorContains(t, err, "minimum autostart interval 1m0s is above template constraint 1h0m0s")
-	})
-
-	t.Run("CreateErrInvalidTz", func(t *testing.T) {
-		t.Parallel()
-		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
-		user := coderdtest.CreateFirstUser(t, client)
-		version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
-		coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
-		template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
-		args := []string{
-			"create",
-			"my-workspace",
-			"--template", template.Name,
-			"--tz", "invalid",
-			"-y",
-		}
-		cmd, root := clitest.New(t, args...)
-		clitest.SetupConfig(t, client, root)
-		pty := ptytest.New(t)
-		cmd.SetIn(pty.Input())
-		cmd.SetOut(pty.Output())
-		err := cmd.Execute()
-		assert.ErrorContains(t, err, "Invalid autostart schedule: Invalid workspace autostart timezone: unknown time zone invalid")
-	})
 
-	t.Run("CreateErrInvalidTTL", func(t *testing.T) {
-		t.Parallel()
-		client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
-		user := coderdtest.CreateFirstUser(t, client)
-		version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
-		coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
-		template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
-		args := []string{
-			"create",
-			"my-workspace",
-			"--template", template.Name,
-			"--ttl", "0s",
-			"-y",
+		ws, err := client.WorkspaceByOwnerAndName(context.Background(), "testuser", "my-workspace", codersdk.WorkspaceOptions{})
+		if assert.NoError(t, err, "expected workspace to be created") {
+			assert.Equal(t, ws.TemplateName, template.Name)
+			if assert.NotNil(t, ws.AutostartSchedule) {
+				assert.Equal(t, *ws.AutostartSchedule, "CRON_TZ=US/Central 30 9 * * Mon-Fri")
+			}
+			if assert.NotNil(t, ws.TTLMillis) {
+				assert.Equal(t, *ws.TTLMillis, 8*time.Hour.Milliseconds())
+			}
 		}
-		cmd, root := clitest.New(t, args...)
-		clitest.SetupConfig(t, client, root)
-		pty := ptytest.New(t)
-		cmd.SetIn(pty.Input())
-		cmd.SetOut(pty.Output())
-		err := cmd.Execute()
-		assert.EqualError(t, err, "TTL must be at least 1 minute")
 	})
 
 	t.Run("CreateFromListWithSkip", func(t *testing.T) {
@@ -197,7 +107,7 @@ func TestCreate(t *testing.T) {
 		user := coderdtest.CreateFirstUser(t, client)
 		version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
 		coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
-		_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
+		template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
 		cmd, root := clitest.New(t, "create", "")
 		clitest.SetupConfig(t, client, root)
 		doneChan := make(chan struct{})
@@ -220,6 +130,12 @@ func TestCreate(t *testing.T) {
 			pty.WriteLine(value)
 		}
 		<-doneChan
+
+		ws, err := client.WorkspaceByOwnerAndName(cmd.Context(), "testuser", "my-workspace", codersdk.WorkspaceOptions{})
+		if assert.NoError(t, err, "expected workspace to be created") {
+			assert.Equal(t, ws.TemplateName, template.Name)
+			assert.Nil(t, ws.AutostartSchedule, "expected workspace autostart schedule to be nil")
+		}
 	})
 
 	t.Run("WithParameter", func(t *testing.T) {