feat: add user quiet hours schedule and restart requirement feature flag (#8115)

This commit is contained in:
Dean Sheather
2023-07-20 06:35:41 -07:00
committed by GitHub
parent 4821e2e6d8
commit dc8b73168e
67 changed files with 4340 additions and 767 deletions

View File

@ -45,6 +45,7 @@ const (
FeatureExternalProvisionerDaemons FeatureName = "external_provisioner_daemons"
FeatureAppearance FeatureName = "appearance"
FeatureAdvancedTemplateScheduling FeatureName = "advanced_template_scheduling"
FeatureTemplateRestartRequirement FeatureName = "template_restart_requirement"
FeatureWorkspaceProxy FeatureName = "workspace_proxy"
)
@ -167,6 +168,7 @@ type DeploymentValues struct {
DisableOwnerWorkspaceExec clibase.Bool `json:"disable_owner_workspace_exec,omitempty" typescript:",notnull"`
ProxyHealthStatusInterval clibase.Duration `json:"proxy_health_status_interval,omitempty" typescript:",notnull"`
EnableTerraformDebugMode clibase.Bool `json:"enable_terraform_debug_mode,omitempty" typescript:",notnull"`
UserQuietHoursSchedule UserQuietHoursScheduleConfig `json:"user_quiet_hours_schedule,omitempty" typescript:",notnull"`
Config clibase.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"`
WriteConfig clibase.Bool `json:"write_config,omitempty" typescript:",notnull"`
@ -344,6 +346,13 @@ type DangerousConfig struct {
AllowAllCors clibase.Bool `json:"allow_all_cors" typescript:",notnull"`
}
type UserQuietHoursScheduleConfig struct {
DefaultSchedule clibase.String `json:"default_schedule" typescript:",notnull"`
// TODO: add WindowDuration and the ability to postpone max_deadline by this
// amount
// WindowDuration clibase.Duration `json:"window_duration" typescript:",notnull"`
}
const (
annotationEnterpriseKey = "enterprise"
annotationSecretKey = "secret"
@ -467,6 +476,11 @@ when required by your organization's security policy.`,
Description: `Tune the behavior of the provisioner, which is responsible for creating, updating, and deleting workspace resources.`,
YAML: "provisioning",
}
deploymentGroupUserQuietHoursSchedule = clibase.Group{
Name: "User Quiet Hours Schedule",
Description: "Allow users to set quiet hours schedules each day for workspaces to avoid workspaces stopping during the day due to template max TTL.",
YAML: "userQuietHoursSchedule",
}
deploymentGroupDangerous = clibase.Group{
Name: "⚠️ Dangerous",
YAML: "dangerous",
@ -1581,6 +1595,16 @@ Write out the current server config as YAML to stdout.`,
Group: &deploymentGroupNetworkingHTTP,
YAML: "proxyHealthInterval",
},
{
Name: "Default Quiet Hours Schedule",
Description: "The default daily cron schedule applied to users that haven't set a custom quiet hours schedule themselves. The quiet hours schedule determines when workspaces will be force stopped due to the template's max TTL, and will round the max TTL up to be within the user's quiet hours window (or default). The format is the same as the standard cron format, but the day-of-month, month and day-of-week must be *. Only one hour and minute can be specified (ranges or comma separated values are not supported).",
Flag: "default-quiet-hours-schedule",
Env: "CODER_QUIET_HOURS_DEFAULT_SCHEDULE",
Default: "",
Value: &c.UserQuietHoursSchedule.DefaultSchedule,
Group: &deploymentGroupUserQuietHoursSchedule,
YAML: "defaultQuietHoursSchedule",
},
}
return opts
}
@ -1782,6 +1806,19 @@ const (
ExperimentSingleTailnet Experiment = "single_tailnet"
ExperimentWorkspaceBuildLogsUI Experiment = "workspace_build_logs_ui"
// ExperimentTemplateRestartRequirement allows template admins to have more
// control over when workspaces created on a template are required to
// restart, and allows users to ensure these restarts never happen during
// their business hours.
//
// Enables:
// - User quiet hours schedule settings
// - Template restart requirement settings
// - Changes the max_deadline algorithm to use restart requirement and user
// quiet hours instead of max_ttl.
ExperimentTemplateRestartRequirement Experiment = "template_restart_requirement"
// Add new experiments here!
// ExperimentExample Experiment = "example"
)

View File

@ -84,9 +84,11 @@ type CreateTemplateRequest struct {
// DefaultTTLMillis allows optionally specifying the default TTL
// for all workspaces created from this template.
DefaultTTLMillis *int64 `json:"default_ttl_ms,omitempty"`
// MaxTTLMillis allows optionally specifying the max lifetime for
// workspaces created from this template.
// TODO(@dean): remove max_ttl once restart_requirement is matured
MaxTTLMillis *int64 `json:"max_ttl_ms,omitempty"`
// RestartRequirement allows optionally specifying the restart requirement
// for workspaces created from this template. This is an enterprise feature.
RestartRequirement *TemplateRestartRequirement `json:"restart_requirement,omitempty"`
// Allow users to cancel in-progress workspace jobs.
// *bool as the default value is "true".

View File

@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/google/uuid"
@ -28,11 +29,13 @@ type Template struct {
Description string `json:"description"`
Icon string `json:"icon"`
DefaultTTLMillis int64 `json:"default_ttl_ms"`
// MaxTTLMillis is an enterprise feature. It's value is only used if your
// license is entitled to use the advanced template scheduling feature.
MaxTTLMillis int64 `json:"max_ttl_ms"`
CreatedByID uuid.UUID `json:"created_by_id" format:"uuid"`
CreatedByName string `json:"created_by_name"`
// TODO(@dean): remove max_ttl once restart_requirement is matured
MaxTTLMillis int64 `json:"max_ttl_ms"`
// RestartRequirement is an enterprise feature. Its value is only used if
// your license is entitled to use the advanced template scheduling feature.
RestartRequirement TemplateRestartRequirement `json:"restart_requirement"`
CreatedByID uuid.UUID `json:"created_by_id" format:"uuid"`
CreatedByName string `json:"created_by_name"`
// AllowUserAutostart and AllowUserAutostop are enterprise-only. Their
// values are only used if your license is entitled to use the advanced
@ -49,6 +52,78 @@ type Template struct {
LockedTTLMillis int64 `json:"locked_ttl_ms"`
}
// WeekdaysToBitmap converts a list of weekdays to a bitmap in accordance with
// the schedule package's rules. The 0th bit is Monday, ..., the 6th bit is
// Sunday. The 7th bit is unused.
func WeekdaysToBitmap(days []string) (uint8, error) {
var bitmap uint8
for _, day := range days {
switch strings.ToLower(day) {
case "monday":
bitmap |= 1 << 0
case "tuesday":
bitmap |= 1 << 1
case "wednesday":
bitmap |= 1 << 2
case "thursday":
bitmap |= 1 << 3
case "friday":
bitmap |= 1 << 4
case "saturday":
bitmap |= 1 << 5
case "sunday":
bitmap |= 1 << 6
default:
return 0, xerrors.Errorf("invalid weekday %q", day)
}
}
return bitmap, nil
}
// BitmapToWeekdays converts a bitmap to a list of weekdays in accordance with
// the schedule package's rules (see above).
func BitmapToWeekdays(bitmap uint8) []string {
var days []string
for i := 0; i < 7; i++ {
if bitmap&(1<<i) != 0 {
switch i {
case 0:
days = append(days, "monday")
case 1:
days = append(days, "tuesday")
case 2:
days = append(days, "wednesday")
case 3:
days = append(days, "thursday")
case 4:
days = append(days, "friday")
case 5:
days = append(days, "saturday")
case 6:
days = append(days, "sunday")
}
}
}
return days
}
type TemplateRestartRequirement struct {
// DaysOfWeek is a list of days of the week on which restarts are required.
// Restarts happen within the user's quiet hours (in their configured
// timezone). If no days are specified, restarts are not required. Weekdays
// cannot be specified twice.
//
// Restarts will only happen on weekdays in this list on weeks which line up
// with Weeks.
DaysOfWeek []string `json:"days_of_week" enums:"monday,tuesday,wednesday,thursday,friday,saturday,sunday"`
// Weeks is the number of weeks between required restarts. Weeks are synced
// across all workspaces (and Coder deployments) using modulo math on a
// hardcoded epoch week of January 2nd, 2023 (the first Monday of 2023).
// Values of 0 or 1 indicate weekly restarts. Values of 2 indicate
// fortnightly restarts, etc.
Weeks int64 `json:"weeks"`
}
type TransitionStats struct {
P50 *int64 `example:"123"`
P95 *int64 `example:"146"`
@ -98,16 +173,18 @@ type UpdateTemplateMeta struct {
Description string `json:"description,omitempty"`
Icon string `json:"icon,omitempty"`
DefaultTTLMillis int64 `json:"default_ttl_ms,omitempty"`
// MaxTTLMillis can only be set if your license includes the advanced
// TODO(@dean): remove max_ttl once restart_requirement is matured
MaxTTLMillis int64 `json:"max_ttl_ms,omitempty"`
// RestartRequirement can only be set if your license includes the advanced
// template scheduling feature. If you attempt to set this value while
// unlicensed, it will be ignored.
MaxTTLMillis int64 `json:"max_ttl_ms,omitempty"`
AllowUserAutostart bool `json:"allow_user_autostart,omitempty"`
AllowUserAutostop bool `json:"allow_user_autostop,omitempty"`
AllowUserCancelWorkspaceJobs bool `json:"allow_user_cancel_workspace_jobs,omitempty"`
FailureTTLMillis int64 `json:"failure_ttl_ms,omitempty"`
InactivityTTLMillis int64 `json:"inactivity_ttl_ms,omitempty"`
LockedTTLMillis int64 `json:"locked_ttl_ms,omitempty"`
RestartRequirement *TemplateRestartRequirement `json:"restart_requirement,omitempty"`
AllowUserAutostart bool `json:"allow_user_autostart,omitempty"`
AllowUserAutostop bool `json:"allow_user_autostop,omitempty"`
AllowUserCancelWorkspaceJobs bool `json:"allow_user_cancel_workspace_jobs,omitempty"`
FailureTTLMillis int64 `json:"failure_ttl_ms,omitempty"`
InactivityTTLMillis int64 `json:"inactivity_ttl_ms,omitempty"`
LockedTTLMillis int64 `json:"locked_ttl_ms,omitempty"`
}
type TemplateExample struct {

View File

@ -84,6 +84,34 @@ type UpdateUserPasswordRequest struct {
Password string `json:"password" validate:"required"`
}
type UserQuietHoursScheduleResponse struct {
RawSchedule string `json:"raw_schedule"`
// UserSet is true if the user has set their own quiet hours schedule. If
// false, the user is using the default schedule.
UserSet bool `json:"user_set"`
// Time is the time of day that the quiet hours window starts in the given
// Timezone each day.
Time string `json:"time"` // HH:mm (24-hour)
Timezone string `json:"timezone"` // raw format from the cron expression, UTC if unspecified
// Next is the next time that the quiet hours window will start.
Next time.Time `json:"next" format:"date-time"`
}
type UpdateUserQuietHoursScheduleRequest struct {
// Schedule is a cron expression that defines when the user's quiet hours
// window is. Schedule must not be empty. For new users, the schedule is set
// to 2am in their browser or computer's timezone. The schedule denotes the
// beginning of a 4 hour window where the workspace is allowed to
// automatically stop or restart due to maintenance or template max TTL.
//
// The schedule must be daily with a single time, and should have a timezone
// specified via a CRON_TZ prefix (otherwise UTC will be used).
//
// If the schedule is empty, the user will be updated to use the default
// schedule.
Schedule string `json:"schedule" validate:"required"`
}
type UpdateRoles struct {
Roles []string `json:"roles" validate:""`
}
@ -364,6 +392,36 @@ func (c *Client) User(ctx context.Context, userIdent string) (User, error) {
return user, json.NewDecoder(res.Body).Decode(&user)
}
// UserQuietHoursSchedule returns the quiet hours settings for the user. This
// endpoint only exists in enterprise editions.
func (c *Client) UserQuietHoursSchedule(ctx context.Context, userIdent string) (UserQuietHoursScheduleResponse, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/quiet-hours", userIdent), nil)
if err != nil {
return UserQuietHoursScheduleResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return UserQuietHoursScheduleResponse{}, ReadBodyAsError(res)
}
var resp UserQuietHoursScheduleResponse
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
// UpdateUserQuietHoursSchedule updates the quiet hours settings for the user.
// This endpoint only exists in enterprise editions.
func (c *Client) UpdateUserQuietHoursSchedule(ctx context.Context, userIdent string, req UpdateUserQuietHoursScheduleRequest) (UserQuietHoursScheduleResponse, error) {
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/quiet-hours", userIdent), req)
if err != nil {
return UserQuietHoursScheduleResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return UserQuietHoursScheduleResponse{}, ReadBodyAsError(res)
}
var resp UserQuietHoursScheduleResponse
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
// Users returns all users according to the request parameters. If no parameters are set,
// the default behavior is to return all users in a single page.
func (c *Client) Users(ctx context.Context, req UsersRequest) (GetUsersResponse, error) {