mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat: add controls to template for determining startup days (#10226)
* feat: template controls which days can autostart * Add unit test to test blocking autostart with DaysOfWeek
This commit is contained in:
@ -86,6 +86,9 @@ func (s *EnterpriseTemplateScheduleStore) Get(ctx context.Context, db database.S
|
||||
DaysOfWeek: uint8(tpl.AutostopRequirementDaysOfWeek),
|
||||
Weeks: tpl.AutostopRequirementWeeks,
|
||||
},
|
||||
AutostartRequirement: agpl.TemplateAutostartRequirement{
|
||||
DaysOfWeek: tpl.AutostartAllowedDays(),
|
||||
},
|
||||
FailureTTL: time.Duration(tpl.FailureTTL),
|
||||
TimeTilDormant: time.Duration(tpl.TimeTilDormant),
|
||||
TimeTilDormantAutoDelete: time.Duration(tpl.TimeTilDormantAutoDelete),
|
||||
@ -107,6 +110,7 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S
|
||||
if int64(opts.DefaultTTL) == tpl.DefaultTTL &&
|
||||
int64(opts.MaxTTL) == tpl.MaxTTL &&
|
||||
int16(opts.AutostopRequirement.DaysOfWeek) == tpl.AutostopRequirementDaysOfWeek &&
|
||||
opts.AutostartRequirement.DaysOfWeek == tpl.AutostartAllowedDays() &&
|
||||
opts.AutostopRequirement.Weeks == tpl.AutostopRequirementWeeks &&
|
||||
int64(opts.FailureTTL) == tpl.FailureTTL &&
|
||||
int64(opts.TimeTilDormant) == tpl.TimeTilDormant &&
|
||||
@ -119,7 +123,12 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S
|
||||
|
||||
err := agpl.VerifyTemplateAutostopRequirement(opts.AutostopRequirement.DaysOfWeek, opts.AutostopRequirement.Weeks)
|
||||
if err != nil {
|
||||
return database.Template{}, err
|
||||
return database.Template{}, xerrors.Errorf("verify autostop requirement: %w", err)
|
||||
}
|
||||
|
||||
err = agpl.VerifyTemplateAutostartRequirement(opts.AutostartRequirement.DaysOfWeek)
|
||||
if err != nil {
|
||||
return database.Template{}, xerrors.Errorf("verify autostart requirement: %w", err)
|
||||
}
|
||||
|
||||
var template database.Template
|
||||
@ -136,9 +145,12 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S
|
||||
MaxTTL: int64(opts.MaxTTL),
|
||||
AutostopRequirementDaysOfWeek: int16(opts.AutostopRequirement.DaysOfWeek),
|
||||
AutostopRequirementWeeks: opts.AutostopRequirement.Weeks,
|
||||
FailureTTL: int64(opts.FailureTTL),
|
||||
TimeTilDormant: int64(opts.TimeTilDormant),
|
||||
TimeTilDormantAutoDelete: int64(opts.TimeTilDormantAutoDelete),
|
||||
// Database stores the inverse of the allowed days of the week.
|
||||
// Make sure the 8th bit is always zeroed out, as there is no 8th day of the week.
|
||||
AutostartBlockDaysOfWeek: int16(^opts.AutostartRequirement.DaysOfWeek & 0b01111111),
|
||||
FailureTTL: int64(opts.FailureTTL),
|
||||
TimeTilDormant: int64(opts.TimeTilDormant),
|
||||
TimeTilDormantAutoDelete: int64(opts.TimeTilDormantAutoDelete),
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("update template schedule: %w", err)
|
||||
|
@ -140,6 +140,89 @@ func TestTemplates(t *testing.T) {
|
||||
require.EqualValues(t, exp, *ws.TTLMillis)
|
||||
})
|
||||
|
||||
t.Run("SetAutostartRequirement", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, user := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAdvancedTemplateScheduling: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
require.Equal(t, []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}, template.AutostartRequirement.DaysOfWeek)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
updated, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||
Name: template.Name,
|
||||
DisplayName: template.DisplayName,
|
||||
Description: template.Description,
|
||||
Icon: template.Icon,
|
||||
AutostartRequirement: &codersdk.TemplateAutostartRequirement{
|
||||
DaysOfWeek: []string{"monday", "saturday"},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"monday", "saturday"}, updated.AutostartRequirement.DaysOfWeek)
|
||||
|
||||
template, err = client.Template(ctx, template.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"monday", "saturday"}, template.AutostartRequirement.DaysOfWeek)
|
||||
|
||||
// Ensure a missing field is a noop
|
||||
updated, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||
Name: template.Name,
|
||||
DisplayName: template.DisplayName,
|
||||
Description: template.Description,
|
||||
Icon: template.Icon + "something",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"monday", "saturday"}, updated.AutostartRequirement.DaysOfWeek)
|
||||
|
||||
template, err = client.Template(ctx, template.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"monday", "saturday"}, template.AutostartRequirement.DaysOfWeek)
|
||||
})
|
||||
|
||||
t.Run("SetInvalidAutostartRequirement", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, user := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
IncludeProvisionerDaemon: true,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAdvancedTemplateScheduling: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
require.Equal(t, []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}, template.AutostartRequirement.DaysOfWeek)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
_, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
|
||||
Name: template.Name,
|
||||
DisplayName: template.DisplayName,
|
||||
Description: template.Description,
|
||||
Icon: template.Icon,
|
||||
AutostartRequirement: &codersdk.TemplateAutostartRequirement{
|
||||
DaysOfWeek: []string{"foobar", "saturday"},
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("SetAutostopRequirement", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
@ -736,6 +736,65 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// Blocked by autostart requirements
|
||||
func TestExecutorAutostartBlocked(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Now()
|
||||
var allowed []string
|
||||
for _, day := range agplschedule.DaysOfWeek {
|
||||
// Skip the day the workspace was created on and if the next day is within 2
|
||||
// hours, skip that too. The cron scheduler will start the workspace every hour,
|
||||
// so it can span into the next day.
|
||||
if day != now.UTC().Weekday() &&
|
||||
day != now.UTC().Add(time.Hour*2).Weekday() {
|
||||
allowed = append(allowed, day.String())
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
sched = must(cron.Weekly("CRON_TZ=UTC 0 * * * *"))
|
||||
tickCh = make(chan time.Time)
|
||||
statsCh = make(chan autobuild.Stats)
|
||||
client, owner = coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
AutobuildTicker: tickCh,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statsCh,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore()),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
|
||||
},
|
||||
})
|
||||
version = coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
|
||||
template = coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(request *codersdk.CreateTemplateRequest) {
|
||||
request.AutostartRequirement = &codersdk.TemplateAutostartRequirement{
|
||||
DaysOfWeek: allowed,
|
||||
}
|
||||
})
|
||||
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
workspace = coderdtest.CreateWorkspace(t, client, owner.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||||
})
|
||||
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
)
|
||||
|
||||
// Given: workspace is stopped
|
||||
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
|
||||
// When: the autobuild executor ticks way into the future
|
||||
go func() {
|
||||
tickCh <- workspace.LatestBuild.CreatedAt.Add(24 * time.Hour)
|
||||
close(tickCh)
|
||||
}()
|
||||
|
||||
// Then: the workspace should not be started.
|
||||
stats := <-statsCh
|
||||
require.NoError(t, stats.Error)
|
||||
require.Len(t, stats.Transitions, 0)
|
||||
}
|
||||
|
||||
func TestWorkspacesFiltering(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@ -911,3 +970,10 @@ func TestWorkspaceLock(t *testing.T) {
|
||||
require.True(t, workspace.LastUsedAt.After(lastUsedAt))
|
||||
})
|
||||
}
|
||||
|
||||
func must[T any](value T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
Reference in New Issue
Block a user