package schedule import ( "context" "time" "github.com/google/uuid" "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/tracing" ) const MaxTemplateAutostopRequirementWeeks = 16 func TemplateAutostopRequirementEpoch(loc *time.Location) time.Time { // The "first week" starts on January 2nd, 2023, which is the first Monday // of 2023. All other weeks are counted using modulo arithmetic from that // date. return time.Date(2023, time.January, 2, 0, 0, 0, 0, loc) } // DaysOfWeek intentionally starts on Monday as opposed to Sunday so the weekend // days are contiguous in the bitmap. This matters greatly when doing restarts // every second week or more to avoid workspaces restarting "at the start" of // the week rather than "at the end" of the week. var DaysOfWeek = []time.Weekday{ time.Monday, time.Tuesday, time.Wednesday, time.Thursday, time.Friday, time.Saturday, time.Sunday, } type TemplateAutostopRequirement struct { // DaysOfWeek is a bitmap of which days of the week the workspace must be // restarted. If fully zero, the workspace is not required to be restarted // ever. // // First bit is Monday, ..., seventh bit is Sunday, eighth bit is unused. DaysOfWeek uint8 // Weeks is the amount of weeks between restarts. If 0 or 1, the workspace // is restarted weekly in accordance with DaysOfWeek. If 2, the workspace is // restarted every other week. And so forth. // // The limit for this value is 16, which is roughly 4 months. // // The "first week" starts on January 2nd, 2023, which is the first Monday // of 2023. All other weeks are counted using modulo arithmetic from that // date. Weeks int64 } // DaysMap returns a map of the days of the week that the workspace must be // restarted. func (r TemplateAutostopRequirement) DaysMap() map[time.Weekday]bool { days := make(map[time.Weekday]bool) for i, day := range DaysOfWeek { days[day] = r.DaysOfWeek&(1< 0b11111111 { return xerrors.New("invalid autostop requirement days, too large") } if weeks < 0 { return xerrors.New("invalid autostop requirement weeks, negative") } if weeks > MaxTemplateAutostopRequirementWeeks { return xerrors.New("invalid autostop requirement weeks, too large") } return nil } type TemplateScheduleOptions struct { UserAutostartEnabled bool `json:"user_autostart_enabled"` UserAutostopEnabled bool `json:"user_autostop_enabled"` DefaultTTL time.Duration `json:"default_ttl"` // TODO(@dean): remove MaxTTL once autostop_requirement is matured and the // default MaxTTL time.Duration `json:"max_ttl"` // UseAutostopRequirement dictates whether the autostop requirement should // be used instead of MaxTTL. This is governed by the feature flag and // licensing. // TODO(@dean): remove this when we remove max_tll UseAutostopRequirement bool // AutostopRequirement dictates when the workspace must be restarted. This // used to be handled by MaxTTL. AutostopRequirement TemplateAutostopRequirement `json:"autostop_requirement"` // FailureTTL dictates the duration after which failed workspaces will be // stopped automatically. FailureTTL time.Duration `json:"failure_ttl"` // TimeTilDormant dictates the duration after which inactive workspaces will // go dormant. TimeTilDormant time.Duration `json:"time_til_dormant"` // TimeTilDormantAutoDelete dictates the duration after which dormant workspaces will be // permanently deleted. TimeTilDormantAutoDelete time.Duration `json:"time_til_dormant_autodelete"` // UpdateWorkspaceLastUsedAt updates the template's workspaces' // last_used_at field. This is useful for preventing updates to the // templates inactivity_ttl immediately triggering a dormant action against // workspaces whose last_used_at field violates the new template // inactivity_ttl threshold. UpdateWorkspaceLastUsedAt bool `json:"update_workspace_last_used_at"` // UpdateWorkspaceDormantAt updates the template's workspaces' // dormant_at field. This is useful for preventing updates to the // templates locked_ttl immediately triggering a delete action against // workspaces whose dormant_at field violates the new template time_til_dormant_autodelete // threshold. UpdateWorkspaceDormantAt bool `json:"update_workspace_dormant_at"` } // TemplateScheduleStore provides an interface for retrieving template // scheduling options set by the template/site admin. type TemplateScheduleStore interface { Get(ctx context.Context, db database.Store, templateID uuid.UUID) (TemplateScheduleOptions, error) Set(ctx context.Context, db database.Store, template database.Template, opts TemplateScheduleOptions) (database.Template, error) } type agplTemplateScheduleStore struct{} var _ TemplateScheduleStore = &agplTemplateScheduleStore{} func NewAGPLTemplateScheduleStore() TemplateScheduleStore { return &agplTemplateScheduleStore{} } func (*agplTemplateScheduleStore) Get(ctx context.Context, db database.Store, templateID uuid.UUID) (TemplateScheduleOptions, error) { ctx, span := tracing.StartSpan(ctx) defer span.End() tpl, err := db.GetTemplateByID(ctx, templateID) if err != nil { return TemplateScheduleOptions{}, err } return TemplateScheduleOptions{ // Disregard the values in the database, since user scheduling is an // enterprise feature. UserAutostartEnabled: true, UserAutostopEnabled: true, DefaultTTL: time.Duration(tpl.DefaultTTL), // Disregard the values in the database, since AutostopRequirement, // FailureTTL, TimeTilDormant, and TimeTilDormantAutoDelete are enterprise features. UseAutostopRequirement: false, MaxTTL: 0, AutostopRequirement: TemplateAutostopRequirement{ DaysOfWeek: 0, Weeks: 0, }, FailureTTL: 0, TimeTilDormant: 0, TimeTilDormantAutoDelete: 0, }, nil } func (*agplTemplateScheduleStore) Set(ctx context.Context, db database.Store, tpl database.Template, opts TemplateScheduleOptions) (database.Template, error) { ctx, span := tracing.StartSpan(ctx) defer span.End() if int64(opts.DefaultTTL) == tpl.DefaultTTL { // Avoid updating the UpdatedAt timestamp if nothing will be changed. return tpl, nil } var template database.Template err := db.InTx(func(db database.Store) error { err := db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{ ID: tpl.ID, UpdatedAt: database.Now(), DefaultTTL: int64(opts.DefaultTTL), // Don't allow changing these settings, but keep the value in the DB (to // avoid clearing settings if the license has an issue). MaxTTL: tpl.MaxTTL, AutostopRequirementDaysOfWeek: tpl.AutostopRequirementDaysOfWeek, AutostopRequirementWeeks: tpl.AutostopRequirementWeeks, AllowUserAutostart: tpl.AllowUserAutostart, AllowUserAutostop: tpl.AllowUserAutostop, FailureTTL: tpl.FailureTTL, TimeTilDormant: tpl.TimeTilDormant, TimeTilDormantAutoDelete: tpl.TimeTilDormantAutoDelete, }) if err != nil { return xerrors.Errorf("update template schedule: %w", err) } template, err = db.GetTemplateByID(ctx, tpl.ID) if err != nil { return xerrors.Errorf("fetch updated template: %w", err) } return nil }, nil) if err != nil { return database.Template{}, err } return template, err }