mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add user quiet hours schedule and restart requirement feature flag (#8115)
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
package schedule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
agpl "github.com/coder/coder/coderd/schedule"
|
||||
)
|
||||
|
||||
// EnterpriseTemplateScheduleStore provides an agpl.TemplateScheduleStore that
|
||||
// has all fields implemented for enterprise customers.
|
||||
type EnterpriseTemplateScheduleStore struct {
|
||||
// UseRestartRequirement decides whether the RestartRequirement field should
|
||||
// be used instead of the MaxTTL field for determining the max deadline of a
|
||||
// workspace build. This value is determined by a feature flag, licensing,
|
||||
// and whether a default user quiet hours schedule is set.
|
||||
UseRestartRequirement atomic.Bool
|
||||
}
|
||||
|
||||
var _ agpl.TemplateScheduleStore = &EnterpriseTemplateScheduleStore{}
|
||||
|
||||
func NewEnterpriseTemplateScheduleStore() *EnterpriseTemplateScheduleStore {
|
||||
return &EnterpriseTemplateScheduleStore{}
|
||||
}
|
||||
|
||||
// Get implements agpl.TemplateScheduleStore.
|
||||
func (s *EnterpriseTemplateScheduleStore) Get(ctx context.Context, db database.Store, templateID uuid.UUID) (agpl.TemplateScheduleOptions, error) {
|
||||
tpl, err := db.GetTemplateByID(ctx, templateID)
|
||||
if err != nil {
|
||||
return agpl.TemplateScheduleOptions{}, err
|
||||
}
|
||||
|
||||
// These extra checks have to be done before the conversion because we lose
|
||||
// precision and signs when converting to the agpl types from the database.
|
||||
if tpl.RestartRequirementDaysOfWeek < 0 {
|
||||
return agpl.TemplateScheduleOptions{}, xerrors.New("invalid restart requirement days, negative")
|
||||
}
|
||||
if tpl.RestartRequirementDaysOfWeek > 0b11111111 {
|
||||
return agpl.TemplateScheduleOptions{}, xerrors.New("invalid restart requirement days, too large")
|
||||
}
|
||||
err = agpl.VerifyTemplateRestartRequirement(uint8(tpl.RestartRequirementDaysOfWeek), tpl.RestartRequirementWeeks)
|
||||
if err != nil {
|
||||
return agpl.TemplateScheduleOptions{}, err
|
||||
}
|
||||
|
||||
return agpl.TemplateScheduleOptions{
|
||||
UserAutostartEnabled: tpl.AllowUserAutostart,
|
||||
UserAutostopEnabled: tpl.AllowUserAutostop,
|
||||
DefaultTTL: time.Duration(tpl.DefaultTTL),
|
||||
MaxTTL: time.Duration(tpl.MaxTTL),
|
||||
UseRestartRequirement: s.UseRestartRequirement.Load(),
|
||||
RestartRequirement: agpl.TemplateRestartRequirement{
|
||||
DaysOfWeek: uint8(tpl.RestartRequirementDaysOfWeek),
|
||||
Weeks: tpl.RestartRequirementWeeks,
|
||||
},
|
||||
FailureTTL: time.Duration(tpl.FailureTTL),
|
||||
InactivityTTL: time.Duration(tpl.InactivityTTL),
|
||||
LockedTTL: time.Duration(tpl.LockedTTL),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Set implements agpl.TemplateScheduleStore.
|
||||
func (*EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.Store, tpl database.Template, opts agpl.TemplateScheduleOptions) (database.Template, error) {
|
||||
if int64(opts.DefaultTTL) == tpl.DefaultTTL &&
|
||||
int64(opts.MaxTTL) == tpl.MaxTTL &&
|
||||
int16(opts.RestartRequirement.DaysOfWeek) == tpl.RestartRequirementDaysOfWeek &&
|
||||
opts.RestartRequirement.Weeks == tpl.RestartRequirementWeeks &&
|
||||
int64(opts.FailureTTL) == tpl.FailureTTL &&
|
||||
int64(opts.InactivityTTL) == tpl.InactivityTTL &&
|
||||
int64(opts.LockedTTL) == tpl.LockedTTL &&
|
||||
opts.UserAutostartEnabled == tpl.AllowUserAutostart &&
|
||||
opts.UserAutostopEnabled == tpl.AllowUserAutostop {
|
||||
// Avoid updating the UpdatedAt timestamp if nothing will be changed.
|
||||
return tpl, nil
|
||||
}
|
||||
|
||||
err := agpl.VerifyTemplateRestartRequirement(opts.RestartRequirement.DaysOfWeek, opts.RestartRequirement.Weeks)
|
||||
if err != nil {
|
||||
return database.Template{}, err
|
||||
}
|
||||
|
||||
var template database.Template
|
||||
err = db.InTx(func(db database.Store) error {
|
||||
err := db.UpdateTemplateScheduleByID(ctx, database.UpdateTemplateScheduleByIDParams{
|
||||
ID: tpl.ID,
|
||||
UpdatedAt: database.Now(),
|
||||
AllowUserAutostart: opts.UserAutostartEnabled,
|
||||
AllowUserAutostop: opts.UserAutostopEnabled,
|
||||
DefaultTTL: int64(opts.DefaultTTL),
|
||||
MaxTTL: int64(opts.MaxTTL),
|
||||
RestartRequirementDaysOfWeek: int16(opts.RestartRequirement.DaysOfWeek),
|
||||
RestartRequirementWeeks: opts.RestartRequirement.Weeks,
|
||||
FailureTTL: int64(opts.FailureTTL),
|
||||
InactivityTTL: int64(opts.InactivityTTL),
|
||||
LockedTTL: int64(opts.LockedTTL),
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("update template schedule: %w", err)
|
||||
}
|
||||
|
||||
// TODO: update all workspace max_deadlines to be within new bounds
|
||||
|
||||
template, err = db.GetTemplateByID(ctx, tpl.ID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get updated template schedule: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return database.Template{}, err
|
||||
}
|
||||
|
||||
return template, nil
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package schedule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
agpl "github.com/coder/coder/coderd/schedule"
|
||||
)
|
||||
|
||||
// enterpriseUserQuietHoursScheduleStore provides an
|
||||
// agpl.UserQuietHoursScheduleStore that has all fields implemented for
|
||||
// enterprise customers.
|
||||
type enterpriseUserQuietHoursScheduleStore struct {
|
||||
defaultSchedule string
|
||||
}
|
||||
|
||||
var _ agpl.UserQuietHoursScheduleStore = &enterpriseUserQuietHoursScheduleStore{}
|
||||
|
||||
func NewEnterpriseUserQuietHoursScheduleStore(defaultSchedule string) (agpl.UserQuietHoursScheduleStore, error) {
|
||||
if defaultSchedule == "" {
|
||||
return nil, xerrors.Errorf("default schedule must be set")
|
||||
}
|
||||
|
||||
s := &enterpriseUserQuietHoursScheduleStore{
|
||||
defaultSchedule: defaultSchedule,
|
||||
}
|
||||
|
||||
_, err := s.parseSchedule(defaultSchedule)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parse default schedule: %w", err)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *enterpriseUserQuietHoursScheduleStore) parseSchedule(rawSchedule string) (agpl.UserQuietHoursScheduleOptions, error) {
|
||||
userSet := true
|
||||
if strings.TrimSpace(rawSchedule) == "" {
|
||||
userSet = false
|
||||
rawSchedule = s.defaultSchedule
|
||||
}
|
||||
|
||||
sched, err := agpl.Daily(rawSchedule)
|
||||
if err != nil {
|
||||
// This shouldn't get hit during Gets, only Sets.
|
||||
return agpl.UserQuietHoursScheduleOptions{}, xerrors.Errorf("parse daily schedule %q: %w", rawSchedule, err)
|
||||
}
|
||||
if strings.HasPrefix(sched.Time(), "cron(") {
|
||||
// Times starting with "cron(" mean it isn't a single time and probably
|
||||
// a range or a list of times as a cron expression. We only support
|
||||
// single times for user quiet hours schedules.
|
||||
// This shouldn't get hit during Gets, only Sets.
|
||||
return agpl.UserQuietHoursScheduleOptions{}, xerrors.Errorf("daily schedule %q has more than one time: %v", rawSchedule, sched.Time())
|
||||
}
|
||||
|
||||
return agpl.UserQuietHoursScheduleOptions{
|
||||
Schedule: sched,
|
||||
UserSet: userSet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *enterpriseUserQuietHoursScheduleStore) Get(ctx context.Context, db database.Store, userID uuid.UUID) (agpl.UserQuietHoursScheduleOptions, error) {
|
||||
user, err := db.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return agpl.UserQuietHoursScheduleOptions{}, xerrors.Errorf("get user by ID: %w", err)
|
||||
}
|
||||
|
||||
return s.parseSchedule(user.QuietHoursSchedule)
|
||||
}
|
||||
|
||||
func (s *enterpriseUserQuietHoursScheduleStore) Set(ctx context.Context, db database.Store, userID uuid.UUID, rawSchedule string) (agpl.UserQuietHoursScheduleOptions, error) {
|
||||
opts, err := s.parseSchedule(rawSchedule)
|
||||
if err != nil {
|
||||
return opts, err
|
||||
}
|
||||
|
||||
// Use the tidy version when storing in the database.
|
||||
rawSchedule = ""
|
||||
if opts.UserSet {
|
||||
rawSchedule = opts.Schedule.String()
|
||||
}
|
||||
_, err = db.UpdateUserQuietHoursSchedule(ctx, database.UpdateUserQuietHoursScheduleParams{
|
||||
ID: userID,
|
||||
QuietHoursSchedule: rawSchedule,
|
||||
})
|
||||
if err != nil {
|
||||
return agpl.UserQuietHoursScheduleOptions{}, xerrors.Errorf("update user quiet hours schedule: %w", err)
|
||||
}
|
||||
|
||||
// TODO(@dean): update max_deadline for all active builds for this user to clamp to
|
||||
// the new schedule.
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
Reference in New Issue
Block a user