chore: refactor time.Duration -> int64 milliseconds for FE consumption (#1944)

* Changes all public-facing codersdk types to use a plain int64 (milliseconds) instead of time.Duration.
* Makes autostart_schedule a *string as it may not be present.
* Adds a utils/ptr package with some useful methods.
This commit is contained in:
Cian Johnston 2022-06-02 11:23:34 +01:00 committed by GitHub
parent 51c420c90a
commit dcf03d8ba3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 287 additions and 148 deletions

View File

@ -51,15 +51,15 @@ func autostartShow() *cobra.Command {
return err
}
if workspace.AutostartSchedule == "" {
if workspace.AutostartSchedule == nil || *workspace.AutostartSchedule == "" {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "not enabled\n")
return nil
}
validSchedule, err := schedule.Weekly(workspace.AutostartSchedule)
validSchedule, err := schedule.Weekly(*workspace.AutostartSchedule)
if err != nil {
// This should never happen.
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "invalid autostart schedule %q for workspace %s: %s\n", workspace.AutostartSchedule, workspace.Name, err.Error())
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "invalid autostart schedule %q for workspace %s: %s\n", *workspace.AutostartSchedule, workspace.Name, err.Error())
return nil
}
@ -110,7 +110,7 @@ func autostartEnable() *cobra.Command {
}
err = client.UpdateWorkspaceAutostart(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
Schedule: validSchedule.String(),
Schedule: &spec,
})
if err != nil {
return err
@ -153,7 +153,7 @@ func autostartDisable() *cobra.Command {
}
err = client.UpdateWorkspaceAutostart(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
Schedule: "",
Schedule: nil,
})
if err != nil {
return err

View File

@ -11,6 +11,7 @@ import (
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
)
@ -34,7 +35,7 @@ func TestAutostart(t *testing.T) {
)
err := client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
Schedule: sched,
Schedule: ptr.Ref(sched),
})
require.NoError(t, err)
@ -76,7 +77,7 @@ func TestAutostart(t *testing.T) {
// 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")
require.Equal(t, sched, *updated.AutostartSchedule, "expected autostart schedule to be set")
// Disable schedule
cmd, root = clitest.New(t, "autostart", "disable", workspace.Name)
@ -90,7 +91,7 @@ func TestAutostart(t *testing.T) {
// 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")
require.Nil(t, updated.AutostartSchedule, "expected autostart schedule to not be set")
})
t.Run("Enable_NotFound", func(t *testing.T) {
@ -155,6 +156,6 @@ func TestAutostart(t *testing.T) {
// Ensure nothing happened
updated, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch updated workspace")
require.Equal(t, expectedSchedule, updated.AutostartSchedule, "expected default autostart schedule")
require.Equal(t, expectedSchedule, *updated.AutostartSchedule, "expected default autostart schedule")
})
}

View File

@ -40,8 +40,8 @@ func TestBump(t *testing.T) {
expectedDeadline := workspace.LatestBuild.Deadline.Add(90 * time.Minute)
// Assert test invariant: workspace build has a deadline set equal to now plus ttl
require.WithinDuration(t, workspace.LatestBuild.Deadline, time.Now().Add(*workspace.TTL), time.Minute)
require.NoError(t, err)
initDeadline := time.Now().Add(time.Duration(*workspace.TTLMillis) * time.Millisecond)
require.WithinDuration(t, initDeadline, workspace.LatestBuild.Deadline, time.Minute)
cmd, root := clitest.New(t, cmdArgs...)
clitest.SetupConfig(t, client, root)
@ -81,8 +81,8 @@ func TestBump(t *testing.T) {
expectedDeadline := workspace.LatestBuild.Deadline.Add(30 * time.Minute)
// Assert test invariant: workspace build has a deadline set equal to now plus ttl
require.WithinDuration(t, workspace.LatestBuild.Deadline, time.Now().Add(*workspace.TTL), time.Minute)
require.NoError(t, err)
initDeadline := time.Now().Add(time.Duration(*workspace.TTLMillis) * time.Millisecond)
require.WithinDuration(t, initDeadline, workspace.LatestBuild.Deadline, time.Minute)
cmd, root := clitest.New(t, cmdArgs...)
clitest.SetupConfig(t, client, root)
@ -121,8 +121,8 @@ func TestBump(t *testing.T) {
require.NoError(t, err)
// Assert test invariant: workspace build has a deadline set equal to now plus ttl
require.WithinDuration(t, workspace.LatestBuild.Deadline, time.Now().Add(*workspace.TTL), time.Minute)
require.NoError(t, err)
initDeadline := time.Now().Add(time.Duration(*workspace.TTLMillis) * time.Millisecond)
require.WithinDuration(t, initDeadline, workspace.LatestBuild.Deadline, time.Minute)
cmd, root := clitest.New(t, cmdArgs...)
clitest.SetupConfig(t, client, root)
@ -147,7 +147,7 @@ func TestBump(t *testing.T) {
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.TTL = nil
cwr.TTLMillis = nil
})
cmdArgs = []string{"bump", workspace.Name}
stdoutBuf = &bytes.Buffer{}
@ -199,8 +199,8 @@ func TestBump(t *testing.T) {
require.NoError(t, err)
// Assert test invariant: workspace build has a deadline set equal to now plus ttl
require.WithinDuration(t, workspace.LatestBuild.Deadline, time.Now().Add(*workspace.TTL), time.Minute)
require.NoError(t, err)
initDeadline := time.Now().Add(time.Duration(*workspace.TTLMillis) * time.Millisecond)
require.WithinDuration(t, initDeadline, workspace.LatestBuild.Deadline, time.Minute)
cmd, root := clitest.New(t, cmdArgs...)
clitest.SetupConfig(t, client, root)

View File

@ -11,6 +11,7 @@ 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"
)
@ -226,7 +227,7 @@ func create() *cobra.Command {
TemplateID: template.ID,
Name: workspaceName,
AutostartSchedule: &schedSpec,
TTL: &ttl,
TTLMillis: ptr.Ref(ttl.Milliseconds()),
ParameterValues: parameters,
})
if err != nil {

View File

@ -11,6 +11,7 @@ import (
"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"
)
@ -87,15 +88,16 @@ func list() *cobra.Command {
duration := time.Now().UTC().Sub(workspace.LatestBuild.Job.CreatedAt).Truncate(time.Second)
autostartDisplay := "-"
if workspace.AutostartSchedule != "" {
if sched, err := schedule.Weekly(workspace.AutostartSchedule); err == nil {
if !ptr.NilOrEmpty(workspace.AutostartSchedule) {
if sched, err := schedule.Weekly(*workspace.AutostartSchedule); err == nil {
autostartDisplay = sched.Cron()
}
}
autostopDisplay := "-"
if workspace.TTL != nil {
autostopDisplay = durationDisplay(*workspace.TTL)
if !ptr.NilOrZero(workspace.TTLMillis) {
dur := time.Duration(*workspace.TTLMillis) * time.Millisecond
autostopDisplay = durationDisplay(dur)
if has, ext := hasExtension(workspace); has {
autostopDisplay += fmt.Sprintf(" (+%s)", durationDisplay(ext.Round(time.Minute)))
}
@ -128,10 +130,11 @@ func hasExtension(ws codersdk.Workspace) (bool, time.Duration) {
if ws.LatestBuild.Deadline.IsZero() {
return false, 0
}
if ws.TTL == nil {
if ws.TTLMillis == nil {
return false, 0
}
delta := ws.LatestBuild.Deadline.Add(-*ws.TTL).Sub(ws.LatestBuild.CreatedAt)
ttl := time.Duration(*ws.TTLMillis) * time.Millisecond
delta := ws.LatestBuild.Deadline.Add(-ttl).Sub(ws.LatestBuild.CreatedAt)
if delta < time.Minute {
return false, 0
}

View File

@ -22,6 +22,7 @@ import (
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/coderd/autobuild/notify"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/cryptorand"
)
@ -290,7 +291,7 @@ func notifyCondition(ctx context.Context, client *codersdk.Client, workspaceID u
return time.Time{}, nil
}
if ws.TTL == nil || *ws.TTL == 0 {
if ptr.NilOrZero(ws.TTLMillis) {
return time.Time{}, nil
}

View File

@ -49,12 +49,13 @@ func ttlShow() *cobra.Command {
return xerrors.Errorf("get workspace: %w", err)
}
if workspace.TTL == nil {
if workspace.TTLMillis == nil || *workspace.TTLMillis == 0 {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "not set\n")
return nil
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s\n", workspace.TTL)
dur := time.Duration(*workspace.TTLMillis) * time.Millisecond
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s\n", dur)
return nil
},
@ -96,10 +97,10 @@ func ttlset() *cobra.Command {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "warning: ttl rounded down to %s\n", truncated)
}
err = client.UpdateWorkspaceTTL(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceTTLRequest{
TTL: &truncated,
})
if err != nil {
millis := truncated.Milliseconds()
if err = client.UpdateWorkspaceTTL(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceTTLRequest{
TTLMillis: &millis,
}); err != nil {
return xerrors.Errorf("update workspace ttl: %w", err)
}
@ -130,7 +131,7 @@ func ttlunset() *cobra.Command {
}
err = client.UpdateWorkspaceTTL(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceTTLRequest{
TTL: nil,
TTLMillis: nil,
})
if err != nil {
return xerrors.Errorf("update workspace ttl: %w", err)

View File

@ -11,6 +11,7 @@ import (
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
)
@ -34,7 +35,7 @@ func TestTTL(t *testing.T) {
)
err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{
TTL: &ttl,
TTLMillis: ptr.Ref(ttl.Milliseconds()),
})
require.NoError(t, err)
@ -73,7 +74,7 @@ func TestTTL(t *testing.T) {
// Ensure ttl updated
updated, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch updated workspace")
require.Equal(t, ttl.Truncate(time.Minute), *updated.TTL)
require.Equal(t, ttl.Truncate(time.Minute), time.Duration(*updated.TTLMillis)*time.Millisecond)
require.Contains(t, stdoutBuf.String(), "warning: ttl rounded down")
// unset schedule
@ -87,7 +88,7 @@ func TestTTL(t *testing.T) {
// Ensure ttl updated
updated, err = client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch updated workspace")
require.Nil(t, updated.TTL, "expected ttl to not be set")
require.Nil(t, updated.TTLMillis, "expected ttl to not be set")
})
t.Run("ZeroInvalid", func(t *testing.T) {
@ -116,7 +117,7 @@ func TestTTL(t *testing.T) {
// Ensure ttl updated
updated, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch updated workspace")
require.Equal(t, ttl.Truncate(time.Minute), *updated.TTL)
require.Equal(t, ttl.Truncate(time.Minute), time.Duration(*updated.TTLMillis)*time.Millisecond)
require.Contains(t, stdoutBuf.String(), "warning: ttl rounded down")
// A TTL of zero is not considered valid.
@ -131,7 +132,7 @@ func TestTTL(t *testing.T) {
// Ensure ttl remains as before
updated, err = client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch updated workspace")
require.Equal(t, ttl.Truncate(time.Minute), *updated.TTL)
require.Equal(t, ttl.Truncate(time.Minute), time.Duration(*updated.TTLMillis)*time.Millisecond)
})
t.Run("Set_NotFound", func(t *testing.T) {

View File

@ -14,6 +14,7 @@ import (
"github.com/coder/coder/coderd/autobuild/schedule"
"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/google/uuid"
@ -44,7 +45,7 @@ func TestExecutorAutostartOK(t *testing.T) {
sched, err := schedule.Weekly("* * * * *")
require.NoError(t, err)
require.NoError(t, client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
Schedule: sched.String(),
Schedule: ptr.Ref(sched.String()),
}))
// When: the autobuild executor ticks
@ -95,7 +96,7 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
sched, err := schedule.Weekly("* * * * *")
require.NoError(t, err)
require.NoError(t, client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
Schedule: sched.String(),
Schedule: ptr.Ref(sched.String()),
}))
// When: the autobuild executor ticks
@ -138,7 +139,7 @@ func TestExecutorAutostartAlreadyRunning(t *testing.T) {
sched, err := schedule.Weekly("* * * * *")
require.NoError(t, err)
require.NoError(t, client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
Schedule: sched.String(),
Schedule: ptr.Ref(sched.String()),
}))
// When: the autobuild executor ticks
@ -316,12 +317,12 @@ func TestExecutorAutostopNotEnabled(t *testing.T) {
})
// Given: we have a user with a workspace that has no TTL set
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.TTL = nil
cwr.TTLMillis = nil
})
)
// Given: workspace has no TTL set
require.Nil(t, workspace.TTL)
require.Nil(t, workspace.TTLMillis)
// Given: workspace is running
require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition)
@ -359,7 +360,7 @@ func TestExecutorWorkspaceDeleted(t *testing.T) {
sched, err := schedule.Weekly("* * * * *")
require.NoError(t, err)
require.NoError(t, client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
Schedule: sched.String(),
Schedule: ptr.Ref(sched.String()),
}))
// Given: workspace is deleted
@ -402,7 +403,7 @@ func TestExecutorWorkspaceAutostartTooEarly(t *testing.T) {
sched, err := schedule.Weekly(futureTimeCron)
require.NoError(t, err)
require.NoError(t, client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
Schedule: sched.String(),
Schedule: ptr.Ref(sched.String()),
}))
// When: the autobuild executor ticks
@ -461,7 +462,7 @@ func TestExecutorWorkspaceAutostopNoWaitChangedMyMind(t *testing.T) {
)
// Given: the user changes their mind and decides their workspace should not auto-stop
err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{TTL: nil})
err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{TTLMillis: nil})
require.NoError(t, err)
// When: the autobuild executor ticks after the deadline

View File

@ -26,6 +26,7 @@ import (
"time"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/util/ptr"
"cloud.google.com/go/compute/metadata"
"github.com/fullsailor/pkcs7"
@ -399,8 +400,8 @@ func CreateWorkspace(t *testing.T, client *codersdk.Client, organization uuid.UU
req := codersdk.CreateWorkspaceRequest{
TemplateID: templateID,
Name: randomUsername(),
AutostartSchedule: ptr("CRON_TZ=US/Central * * * * *"),
TTL: ptr(8 * time.Hour),
AutostartSchedule: ptr.Ref("CRON_TZ=US/Central * * * * *"),
TTLMillis: ptr.Ref((8 * time.Hour).Milliseconds()),
}
for _, mutator := range mutators {
mutator(&req)
@ -602,7 +603,3 @@ type roundTripper func(req *http.Request) (*http.Response, error)
func (r roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return r(req)
}
func ptr[T any](x T) *T {
return &x
}

23
coderd/util/ptr/ptr.go Normal file
View File

@ -0,0 +1,23 @@
// Package ptr contains some utility methods related to pointers.
package ptr
import "golang.org/x/exp/constraints"
type number interface {
constraints.Integer | constraints.Float
}
// Ref returns a reference to v.
func Ref[T any](v T) *T {
return &v
}
// NilOrEmpty returns true if s is nil or the empty string.
func NilOrEmpty(s *string) bool {
return s == nil || *s == ""
}
// NilOrZero returns true if v is nil or 0.
func NilOrZero[T number](v *T) bool {
return v == nil || *v == 0
}

View File

@ -0,0 +1,81 @@
package ptr_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/coder/coder/coderd/util/ptr"
)
func Test_Ref_Deref(t *testing.T) {
t.Parallel()
t.Run("String", func(t *testing.T) {
t.Parallel()
val := "test"
p := ptr.Ref(val)
assert.Equal(t, &val, p)
})
t.Run("Bool", func(t *testing.T) {
t.Parallel()
val := true
p := ptr.Ref(val)
assert.Equal(t, &val, p)
})
t.Run("Int64", func(t *testing.T) {
t.Parallel()
val := int64(42)
p := ptr.Ref(val)
assert.Equal(t, &val, p)
})
t.Run("Float64", func(t *testing.T) {
t.Parallel()
val := float64(3.14159)
p := ptr.Ref(val)
assert.Equal(t, &val, p)
})
}
func Test_NilOrEmpty(t *testing.T) {
t.Parallel()
nilString := (*string)(nil)
emptyString := ""
nonEmptyString := "hi"
assert.True(t, ptr.NilOrEmpty(nilString))
assert.True(t, ptr.NilOrEmpty(&emptyString))
assert.False(t, ptr.NilOrEmpty(&nonEmptyString))
}
func Test_NilOrZero(t *testing.T) {
t.Parallel()
nilInt64 := (*int64)(nil)
nilFloat64 := (*float64)(nil)
nilDuration := (*time.Duration)(nil)
zeroInt64 := int64(0)
zeroFloat64 := float64(0.0)
zeroDuration := time.Duration(0)
nonZeroInt64 := int64(1)
nonZeroFloat64 := float64(3.14159)
nonZeroDuration := time.Hour
assert.True(t, ptr.NilOrZero(nilInt64))
assert.True(t, ptr.NilOrZero(nilFloat64))
assert.True(t, ptr.NilOrZero(nilDuration))
assert.True(t, ptr.NilOrZero(&zeroInt64))
assert.True(t, ptr.NilOrZero(&zeroFloat64))
assert.True(t, ptr.NilOrZero(&zeroDuration))
assert.False(t, ptr.NilOrZero(&nonZeroInt64))
assert.False(t, ptr.NilOrZero(&nonZeroFloat64))
assert.False(t, ptr.NilOrZero(&nonZeroDuration))
}

View File

@ -25,6 +25,7 @@ import (
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
)
@ -345,7 +346,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
dbAutostartSchedule.String = *createWorkspace.AutostartSchedule
}
dbTTL, err := validWorkspaceTTL(createWorkspace.TTL)
dbTTL, err := validWorkspaceTTLMillis(createWorkspace.TTLMillis)
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "validate workspace ttl",
@ -527,20 +528,15 @@ func (api *API) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) {
return
}
var dbSched sql.NullString
if req.Schedule != "" {
validSched, err := schedule.Weekly(req.Schedule)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("invalid autostart schedule: %s", err),
})
return
}
dbSched.String = validSched.String()
dbSched.Valid = true
dbSched, err := validWorkspaceSchedule(req.Schedule)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("invalid autostart schedule: %s", err),
})
return
}
err := api.Database.UpdateWorkspaceAutostart(r.Context(), database.UpdateWorkspaceAutostartParams{
err = api.Database.UpdateWorkspaceAutostart(r.Context(), database.UpdateWorkspaceAutostartParams{
ID: workspace.ID,
AutostartSchedule: dbSched,
})
@ -564,7 +560,7 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
return
}
dbTTL, err := validWorkspaceTTL(req.TTL)
dbTTL, err := validWorkspaceTTLMillis(req.TTLMillis)
if err != nil {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Message: "validate workspace ttl",
@ -830,13 +826,18 @@ func convertWorkspaces(ctx context.Context, db database.Store, workspaces []data
}
return apiWorkspaces, nil
}
func convertWorkspace(
workspace database.Workspace,
workspaceBuild database.WorkspaceBuild,
job database.ProvisionerJob,
template database.Template,
owner database.User) codersdk.Workspace {
var autostartSchedule *string
if workspace.AutostartSchedule.Valid {
autostartSchedule = &workspace.AutostartSchedule.String
}
ttlMillis := convertWorkspaceTTLMillis(workspace.Ttl)
return codersdk.Workspace{
ID: workspace.ID,
CreatedAt: workspace.CreatedAt,
@ -848,25 +849,27 @@ func convertWorkspace(
TemplateName: template.Name,
Outdated: workspaceBuild.TemplateVersionID.String() != template.ActiveVersionID.String(),
Name: workspace.Name,
AutostartSchedule: workspace.AutostartSchedule.String,
TTL: convertSQLNullInt64(workspace.Ttl),
AutostartSchedule: autostartSchedule,
TTLMillis: ttlMillis,
}
}
func convertSQLNullInt64(i sql.NullInt64) *time.Duration {
func convertWorkspaceTTLMillis(i sql.NullInt64) *int64 {
if !i.Valid {
return nil
}
return (*time.Duration)(&i.Int64)
millis := time.Duration(i.Int64).Milliseconds()
return &millis
}
func validWorkspaceTTL(ttl *time.Duration) (sql.NullInt64, error) {
if ttl == nil {
func validWorkspaceTTLMillis(millis *int64) (sql.NullInt64, error) {
if ptr.NilOrZero(millis) {
return sql.NullInt64{}, nil
}
truncated := ttl.Truncate(time.Minute)
dur := time.Duration(*millis) * time.Millisecond
truncated := dur.Truncate(time.Minute)
if truncated < time.Minute {
return sql.NullInt64{}, xerrors.New("ttl must be at least one minute")
}
@ -902,3 +905,19 @@ func validWorkspaceDeadline(old, new time.Time) error {
return nil
}
func validWorkspaceSchedule(s *string) (sql.NullString, error) {
if ptr.NilOrEmpty(s) {
return sql.NullString{}, nil
}
_, err := schedule.Weekly(*s)
if err != nil {
return sql.NullString{}, err
}
return sql.NullString{
Valid: true,
String: *s,
}, nil
}

View File

@ -8,6 +8,7 @@ import (
"time"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/util/ptr"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
@ -177,8 +178,8 @@ func TestPostWorkspacesByOrganization(t *testing.T) {
req := codersdk.CreateWorkspaceRequest{
TemplateID: template.ID,
Name: "testing",
AutostartSchedule: ptr("CRON_TZ=US/Central * * * * *"),
TTL: ptr(59 * time.Second),
AutostartSchedule: ptr.Ref("CRON_TZ=US/Central * * * * *"),
TTLMillis: ptr.Ref((59 * time.Second).Milliseconds()),
}
_, err := client.CreateWorkspace(context.Background(), template.OrganizationID, req)
require.Error(t, err)
@ -197,8 +198,8 @@ func TestPostWorkspacesByOrganization(t *testing.T) {
req := codersdk.CreateWorkspaceRequest{
TemplateID: template.ID,
Name: "testing",
AutostartSchedule: ptr("CRON_TZ=US/Central * * * * *"),
TTL: ptr(24*7*time.Hour + time.Minute),
AutostartSchedule: ptr.Ref("CRON_TZ=US/Central * * * * *"),
TTLMillis: ptr.Ref((24*7*time.Hour + time.Minute).Milliseconds()),
}
_, err := client.CreateWorkspace(context.Background(), template.OrganizationID, req)
require.Error(t, err)
@ -451,7 +452,7 @@ func TestWorkspaceUpdateAutostart(t *testing.T) {
testCases := []struct {
name string
schedule string
schedule *string
expectedError string
at time.Time
expectedNext time.Time
@ -459,12 +460,12 @@ func TestWorkspaceUpdateAutostart(t *testing.T) {
}{
{
name: "disable autostart",
schedule: "",
schedule: ptr.Ref(""),
expectedError: "",
},
{
name: "friday to monday",
schedule: "CRON_TZ=Europe/Dublin 30 9 * * 1-5",
schedule: ptr.Ref("CRON_TZ=Europe/Dublin 30 9 * * 1-5"),
expectedError: "",
at: time.Date(2022, 5, 6, 9, 31, 0, 0, dublinLoc),
expectedNext: time.Date(2022, 5, 9, 9, 30, 0, 0, dublinLoc),
@ -472,7 +473,7 @@ func TestWorkspaceUpdateAutostart(t *testing.T) {
},
{
name: "monday to tuesday",
schedule: "CRON_TZ=Europe/Dublin 30 9 * * 1-5",
schedule: ptr.Ref("CRON_TZ=Europe/Dublin 30 9 * * 1-5"),
expectedError: "",
at: time.Date(2022, 5, 9, 9, 31, 0, 0, dublinLoc),
expectedNext: time.Date(2022, 5, 10, 9, 30, 0, 0, dublinLoc),
@ -481,7 +482,7 @@ func TestWorkspaceUpdateAutostart(t *testing.T) {
{
// DST in Ireland began on Mar 27 in 2022 at 0100. Forward 1 hour.
name: "DST start",
schedule: "CRON_TZ=Europe/Dublin 30 9 * * *",
schedule: ptr.Ref("CRON_TZ=Europe/Dublin 30 9 * * *"),
expectedError: "",
at: time.Date(2022, 3, 26, 9, 31, 0, 0, dublinLoc),
expectedNext: time.Date(2022, 3, 27, 9, 30, 0, 0, dublinLoc),
@ -490,7 +491,7 @@ func TestWorkspaceUpdateAutostart(t *testing.T) {
{
// DST in Ireland ends on Oct 30 in 2022 at 0200. Back 1 hour.
name: "DST end",
schedule: "CRON_TZ=Europe/Dublin 30 9 * * *",
schedule: ptr.Ref("CRON_TZ=Europe/Dublin 30 9 * * *"),
expectedError: "",
at: time.Date(2022, 10, 29, 9, 31, 0, 0, dublinLoc),
expectedNext: time.Date(2022, 10, 30, 9, 30, 0, 0, dublinLoc),
@ -498,17 +499,17 @@ func TestWorkspaceUpdateAutostart(t *testing.T) {
},
{
name: "invalid location",
schedule: "CRON_TZ=Imaginary/Place 30 9 * * 1-5",
schedule: ptr.Ref("CRON_TZ=Imaginary/Place 30 9 * * 1-5"),
expectedError: "status code 500: invalid autostart schedule: parse schedule: provided bad location Imaginary/Place: unknown time zone Imaginary/Place",
},
{
name: "invalid schedule",
schedule: "asdf asdf asdf ",
schedule: ptr.Ref("asdf asdf asdf "),
expectedError: `status code 500: invalid autostart schedule: validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ=<timezone> prefix`,
},
{
name: "only 3 values",
schedule: "CRON_TZ=Europe/Dublin 30 9 *",
schedule: ptr.Ref("CRON_TZ=Europe/Dublin 30 9 *"),
expectedError: `status code 500: invalid autostart schedule: validate weekly schedule: expected schedule to consist of 5 fields with an optional CRON_TZ=<timezone> prefix`,
},
}
@ -526,7 +527,7 @@ func TestWorkspaceUpdateAutostart(t *testing.T) {
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.AutostartSchedule = nil
cwr.TTL = nil
cwr.TTLMillis = nil
})
)
@ -547,12 +548,14 @@ func TestWorkspaceUpdateAutostart(t *testing.T) {
updated, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch updated workspace")
require.Equal(t, testCase.schedule, updated.AutostartSchedule, "expected autostart schedule to equal requested")
if testCase.schedule == "" {
if testCase.schedule == nil || *testCase.schedule == "" {
require.Nil(t, updated.AutostartSchedule)
return
}
sched, err := schedule.Weekly(updated.AutostartSchedule)
require.EqualValues(t, *testCase.schedule, *updated.AutostartSchedule, "expected autostart schedule to equal requested")
sched, err := schedule.Weekly(*updated.AutostartSchedule)
require.NoError(t, err, "parse returned schedule")
next := sched.Next(testCase.at)
@ -569,7 +572,7 @@ func TestWorkspaceUpdateAutostart(t *testing.T) {
_ = coderdtest.CreateFirstUser(t, client)
wsid = uuid.New()
req = codersdk.UpdateWorkspaceAutostartRequest{
Schedule: "9 30 1-5",
Schedule: ptr.Ref("9 30 1-5"),
}
)
@ -586,32 +589,32 @@ func TestWorkspaceUpdateTTL(t *testing.T) {
testCases := []struct {
name string
ttl *time.Duration
ttlMillis *int64
expectedError string
}{
{
name: "disable ttl",
ttl: nil,
ttlMillis: nil,
expectedError: "",
},
{
name: "below minimum ttl",
ttl: ptr(30 * time.Second),
ttlMillis: ptr.Ref((30 * time.Second).Milliseconds()),
expectedError: "ttl must be at least one minute",
},
{
name: "minimum ttl",
ttl: ptr(time.Minute),
ttlMillis: ptr.Ref(time.Minute.Milliseconds()),
expectedError: "",
},
{
name: "maximum ttl",
ttl: ptr(24 * 7 * time.Hour),
ttlMillis: ptr.Ref((24 * 7 * time.Hour).Milliseconds()),
expectedError: "",
},
{
name: "above maximum ttl",
ttl: ptr(24*7*time.Hour + time.Minute),
ttlMillis: ptr.Ref((24*7*time.Hour + time.Minute).Milliseconds()),
expectedError: "ttl must be less than 7 days",
},
}
@ -629,15 +632,15 @@ func TestWorkspaceUpdateTTL(t *testing.T) {
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.AutostartSchedule = nil
cwr.TTL = nil
cwr.TTLMillis = nil
})
)
// ensure test invariant: new workspaces have no autostop schedule.
require.Nil(t, workspace.TTL, "expected newly-minted workspace to have no TTL")
require.Nil(t, workspace.TTLMillis, "expected newly-minted workspace to have no TTL")
err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{
TTL: testCase.ttl,
TTLMillis: testCase.ttlMillis,
})
if testCase.expectedError != "" {
@ -650,7 +653,7 @@ func TestWorkspaceUpdateTTL(t *testing.T) {
updated, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch updated workspace")
require.Equal(t, testCase.ttl, updated.TTL, "expected autostop ttl to equal requested")
require.Equal(t, testCase.ttlMillis, updated.TTLMillis, "expected autostop ttl to equal requested")
})
}
@ -661,7 +664,7 @@ func TestWorkspaceUpdateTTL(t *testing.T) {
_ = coderdtest.CreateFirstUser(t, client)
wsid = uuid.New()
req = codersdk.UpdateWorkspaceTTLRequest{
TTL: ptr(time.Hour),
TTLMillis: ptr.Ref(time.Hour.Milliseconds()),
}
)
@ -685,8 +688,8 @@ func TestWorkspaceExtend(t *testing.T) {
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
extend = 90 * time.Minute
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
oldDeadline = time.Now().Add(*workspace.TTL).UTC()
newDeadline = time.Now().Add(*workspace.TTL + extend).UTC()
oldDeadline = time.Now().Add(time.Duration(*workspace.TTLMillis) * time.Millisecond).UTC()
newDeadline = time.Now().Add(time.Duration(*workspace.TTLMillis)*time.Millisecond + extend).UTC()
)
workspace, err := client.Workspace(ctx, workspace.ID)
@ -761,7 +764,3 @@ func mustLocation(t *testing.T, location string) *time.Location {
return loc
}
func ptr[T any](x T) *T {
return &x
}

View File

@ -65,10 +65,10 @@ type CreateTemplateRequest struct {
// CreateWorkspaceRequest provides options for creating a new workspace.
type CreateWorkspaceRequest struct {
TemplateID uuid.UUID `json:"template_id" validate:"required"`
Name string `json:"name" validate:"username,required"`
AutostartSchedule *string `json:"autostart_schedule"`
TTL *time.Duration `json:"ttl"`
TemplateID uuid.UUID `json:"template_id" validate:"required"`
Name string `json:"name" validate:"username,required"`
AutostartSchedule *string `json:"autostart_schedule"`
TTLMillis *int64 `json:"ttl_ms,omitempty"`
// ParameterValues allows for additional parameters to be provided
// during the initial provision.
ParameterValues []CreateParameterRequest `json:"parameter_values,omitempty"`

View File

@ -26,8 +26,8 @@ type Workspace struct {
LatestBuild WorkspaceBuild `json:"latest_build"`
Outdated bool `json:"outdated"`
Name string `json:"name"`
AutostartSchedule string `json:"autostart_schedule"`
TTL *time.Duration `json:"ttl"`
AutostartSchedule *string `json:"autostart_schedule,omitempty"`
TTLMillis *int64 `json:"ttl_ms,omitempty"`
}
// CreateWorkspaceBuildRequest provides options to update the latest workspace build.
@ -139,7 +139,7 @@ func (c *Client) WatchWorkspace(ctx context.Context, id uuid.UUID) (<-chan Works
// UpdateWorkspaceAutostartRequest is a request to update a workspace's autostart schedule.
type UpdateWorkspaceAutostartRequest struct {
Schedule string `json:"schedule"`
Schedule *string `json:"schedule"`
}
// UpdateWorkspaceAutostart sets the autostart schedule for workspace by id.
@ -159,7 +159,7 @@ func (c *Client) UpdateWorkspaceAutostart(ctx context.Context, id uuid.UUID, req
// UpdateWorkspaceTTLRequest is a request to update a workspace's TTL.
type UpdateWorkspaceTTLRequest struct {
TTL *time.Duration `json:"ttl"`
TTLMillis *int64 `json:"ttl_ms"`
}
// UpdateWorkspaceTTL sets the ttl for workspace by id.

View File

@ -33,7 +33,7 @@ variable "region" {
description = "What region should your workspace live in?"
default = "us-east-1"
validation {
condition = contains([
condition = contains([
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",

View File

@ -30,7 +30,7 @@ variable "region" {
description = "What region should your workspace live in?"
default = "us-east-1"
validation {
condition = contains([
condition = contains([
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",

View File

@ -101,8 +101,7 @@ export interface CreateWorkspaceRequest {
readonly template_id: string
readonly name: string
readonly autostart_schedule?: string
// This is likely an enum in an external package ("time.Duration")
readonly ttl?: number
readonly ttl_ms?: number
readonly parameter_values?: CreateParameterRequest[]
}
@ -301,13 +300,12 @@ export interface UpdateUserProfileRequest {
// From codersdk/workspaces.go:141:6
export interface UpdateWorkspaceAutostartRequest {
readonly schedule: string
readonly schedule?: string
}
// From codersdk/workspaces.go:161:6
export interface UpdateWorkspaceTTLRequest {
// This is likely an enum in an external package ("time.Duration")
readonly ttl?: number
readonly ttl_ms?: number
}
// From codersdk/files.go:16:6
@ -372,9 +370,8 @@ export interface Workspace {
readonly latest_build: WorkspaceBuild
readonly outdated: boolean
readonly name: string
readonly autostart_schedule: string
// This is likely an enum in an external package ("time.Duration")
readonly ttl?: number
readonly autostart_schedule?: string
readonly ttl_ms?: number
}
// From codersdk/workspaceresources.go:31:6

View File

@ -19,6 +19,20 @@ export default {
const Template: Story<WorkspaceScheduleProps> = (args) => <WorkspaceSchedule {...args} />
export const NoScheduleNoTTL = Template.bind({})
NoScheduleNoTTL.args = {
workspace: {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
transition: "stop",
},
autostart_schedule: undefined,
ttl_ms: undefined,
},
}
export const NoTTL = Template.bind({})
NoTTL.args = {
workspace: {
@ -29,7 +43,7 @@ NoTTL.args = {
// SEE: #1834
deadline: "0001-01-01T00:00:00Z",
},
ttl: undefined,
ttl_ms: undefined,
},
}
@ -42,7 +56,7 @@ ShutdownSoon.args = {
deadline: dayjs().add(ONE, "hour").utc().format(),
transition: "start",
},
ttl: 2 * 60 * 60 * 1000 * 1_000_000, // 2 hours
ttl_ms: 2 * 60 * 60 * 1000, // 2 hours
},
}
@ -56,7 +70,7 @@ ShutdownLong.args = {
deadline: dayjs().add(SEVEN, "days").utc().format(),
transition: "start",
},
ttl: 7 * 24 * 60 * 60 * 1000 * 1_000_000, // 7 days
ttl_ms: 7 * 24 * 60 * 60 * 1000, // 7 days
},
}
@ -69,7 +83,7 @@ WorkspaceOffShort.args = {
...Mocks.MockWorkspaceBuild,
transition: "stop",
},
ttl: 2 * 60 * 60 * 1000 * 1_000_000, // 2 hours
ttl_ms: 2 * 60 * 60 * 1000, // 2 hours
},
}
@ -82,6 +96,6 @@ WorkspaceOffLong.args = {
...Mocks.MockWorkspaceBuild,
transition: "stop",
},
ttl: 2 * 365 * 24 * 60 * 60 * 1000 * 1_000_000, // 2 years
ttl_ms: 2 * 365 * 24 * 60 * 60 * 1000, // 2 years
},
}

View File

@ -20,13 +20,13 @@ dayjs.extend(duration)
dayjs.extend(relativeTime)
export const Language = {
autoStartDisplay: (schedule: string): string => {
autoStartDisplay: (schedule: string | undefined): string => {
if (schedule) {
return cronstrue.toString(stripTimezone(schedule), { throwExceptionOnParseError: false })
}
return "Manual"
},
autoStartLabel: (schedule: string): string => {
autoStartLabel: (schedule: string | undefined): string => {
const prefix = "Start"
if (schedule) {
@ -40,7 +40,7 @@ export const Language = {
// a mannual shutdown has a deadline of '"0001-01-01T00:00:00Z"'
// SEE: #1834
const hasDeadline = deadline.year() > 1
const ttl = workspace.ttl
const ttl = workspace.ttl_ms
if (isWorkspaceOn(workspace) && hasDeadline) {
// Workspace is on --> derive from latest_build.deadline. Note that the
@ -61,7 +61,7 @@ export const Language = {
} else {
// The workspace has a ttl set, but is either in an unknown state or is
// not running. Therefore, we derive from workspace.ttl.
const duration = dayjs.duration(ttl / 1_000_000, "milliseconds")
const duration = dayjs.duration(ttl, "milliseconds")
return `${duration.humanize()} after start`
}
},

View File

@ -123,7 +123,7 @@ describe("WorkspaceSchedulePage", () => {
ttl: 0,
},
{
ttl: undefined,
ttl_ms: undefined,
},
],
[
@ -133,7 +133,7 @@ describe("WorkspaceSchedulePage", () => {
ttl: 2,
},
{
ttl: 7_200_000_000_000,
ttl_ms: 7_200_000,
},
],
[
@ -143,7 +143,7 @@ describe("WorkspaceSchedulePage", () => {
ttl: 8,
},
{
ttl: 28_800_000_000_000,
ttl_ms: 28_800_000,
},
],
])(`formValuesToTTLRequest(%p) returns %p`, (values, request) => {
@ -157,8 +157,8 @@ describe("WorkspaceSchedulePage", () => {
[
{
...Mocks.MockWorkspace,
autostart_schedule: "",
ttl: undefined,
autostart_schedule: undefined,
ttl_ms: undefined,
},
{
sunday: false,
@ -179,7 +179,7 @@ describe("WorkspaceSchedulePage", () => {
{
...Mocks.MockWorkspace,
autostart_schedule: "",
ttl: 7_200_000_000_000,
ttl_ms: 7_200_000,
},
{
sunday: false,
@ -203,7 +203,7 @@ describe("WorkspaceSchedulePage", () => {
{
...Mocks.MockWorkspace,
autostart_schedule: "CRON_TZ=UTC 30 9 * * 1-5",
ttl: 7_200_000_000_000,
ttl_ms: 7_200_000,
},
{
sunday: false,
@ -224,7 +224,7 @@ describe("WorkspaceSchedulePage", () => {
{
...Mocks.MockWorkspace,
autostart_schedule: "CRON_TZ=Canada/Eastern 20 16 * * 1,3-4,6",
ttl: 28_800_000_000_000,
ttl_ms: 28_800_000,
},
{
sunday: false,

View File

@ -87,13 +87,13 @@ export const formValuesToAutoStartRequest = (
export const formValuesToTTLRequest = (values: WorkspaceScheduleFormValues): TypesGen.UpdateWorkspaceTTLRequest => {
return {
// minutes to nanoseconds
ttl: values.ttl ? values.ttl * 60 * 60 * 1000 * 1_000_000 : undefined,
ttl_ms: values.ttl ? values.ttl * 60 * 60 * 1000 : undefined,
}
}
export const workspaceToInitialValues = (workspace: TypesGen.Workspace): WorkspaceScheduleFormValues => {
const schedule = workspace.autostart_schedule
const ttl = workspace.ttl ? workspace.ttl / (1_000_000 * 1000 * 60 * 60) : 0
const ttl = workspace.ttl_ms ? workspace.ttl_ms / (1000 * 60 * 60) : 0
if (!schedule) {
return {

View File

@ -164,7 +164,7 @@ export const MockWorkspace: TypesGen.Workspace = {
owner_id: MockUser.id,
owner_name: MockUser.username,
autostart_schedule: MockWorkspaceAutostartEnabled.schedule,
ttl: 2 * 60 * 60 * 1000 * 1_000_000, // 2 hours as nanoseconds
ttl_ms: 2 * 60 * 60 * 1000, // 2 hours as milliseconds
latest_build: MockWorkspaceBuild,
}