From 7ce08a14c121425eedeb222867aa2f1c9fc1ec22 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 20 Dec 2023 13:48:12 +0000 Subject: [PATCH 01/19] fix: make template push a superset of template create --- cli/templatecreate.go | 73 +++++++++++++++++++++++++ cli/templatepush.go | 124 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 182 insertions(+), 15 deletions(-) diff --git a/cli/templatecreate.go b/cli/templatecreate.go index 51a4c33cfa226..8cec28a187e00 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -1,6 +1,7 @@ package cli import ( + "context" "errors" "fmt" "io" @@ -46,6 +47,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { r.InitClient(client), ), Handler: func(inv *clibase.Invocation) error { +<<<<<<< HEAD isTemplateSchedulingOptionsSet := failureTTL != 0 || dormancyThreshold != 0 || dormancyAutoDeletion != 0 || maxTTL != 0 if isTemplateSchedulingOptionsSet || requireActiveVersion { @@ -79,6 +81,19 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { return xerrors.Errorf("your license is not entitled to use enterprise access control, so you cannot set --require-active-version") } } +======= + err := handleEntitlements(inv.Context(), handleEntitlementsArgs{ + client: client, + requireActiveVersion: requireActiveVersion, + defaultTTL: defaultTTL, + failureTTL: failureTTL, + dormancyThreshold: dormancyThreshold, + dormancyAutoDeletion: dormancyAutoDeletion, + maxTTL: maxTTL, + }) + if err != nil { + return err +>>>>>>> 7b0afe8e9 (fix: make template push a superset of template create) } organization, err := CurrentOrganization(inv, client) @@ -357,3 +372,61 @@ func ParseProvisionerTags(rawTags []string) (map[string]string, error) { } return tags, nil } + +type handleEntitlementsArgs struct { + client *codersdk.Client + requireActiveVersion bool + defaultTTL time.Duration + failureTTL time.Duration + dormancyThreshold time.Duration + dormancyAutoDeletion time.Duration + maxTTL time.Duration +} + +func handleEntitlements(ctx context.Context, args handleEntitlementsArgs) error { + isTemplateSchedulingOptionsSet := args.failureTTL != 0 || args.dormancyThreshold != 0 || args.dormancyAutoDeletion != 0 || args.maxTTL != 0 + + if isTemplateSchedulingOptionsSet || args.requireActiveVersion { + if args.failureTTL != 0 || args.dormancyThreshold != 0 || args.dormancyAutoDeletion != 0 { + // This call can be removed when workspace_actions is no longer experimental + experiments, exErr := args.client.Experiments(ctx) + if exErr != nil { + return xerrors.Errorf("get experiments: %w", exErr) + } + + if !experiments.Enabled(codersdk.ExperimentWorkspaceActions) { + return xerrors.Errorf("--failure-ttl, --dormancy-threshold, and --dormancy-auto-deletion are experimental features. Use the workspace_actions CODER_EXPERIMENTS flag to set these configuration values.") + } + } + + entitlements, err := args.client.Entitlements(ctx) + if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusNotFound { + return xerrors.Errorf("your deployment appears to be an AGPL deployment, so you cannot set enterprise-only flags") + } else if err != nil { + return xerrors.Errorf("get entitlements: %w", err) + } + + if isTemplateSchedulingOptionsSet { + if !entitlements.Features[codersdk.FeatureAdvancedTemplateScheduling].Enabled { + return xerrors.Errorf("your license is not entitled to use advanced template scheduling, so you cannot set --failure-ttl, --inactivity-ttl, or --max-ttl") + } + } + + if args.requireActiveVersion { + if !entitlements.Features[codersdk.FeatureAccessControl].Enabled { + return xerrors.Errorf("your license is not entitled to use enterprise access control, so you cannot set --require-active-version") + } + + experiments, exErr := args.client.Experiments(ctx) + if exErr != nil { + return xerrors.Errorf("get experiments: %w", exErr) + } + + if !experiments.Enabled(codersdk.ExperimentTemplateUpdatePolicies) { + return xerrors.Errorf("--require-active-version is an experimental feature, contact an administrator to enable the 'template_update_policies' experiment on your Coder server") + } + } + } + + return nil +} diff --git a/cli/templatepush.go b/cli/templatepush.go index 4c903ef7ca4d9..e06fb589e2c49 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -2,12 +2,15 @@ package cli import ( "bufio" + "errors" "fmt" "io" + "net/http" "os" "path/filepath" "strings" "time" + "unicode/utf8" "github.com/briandowns/spinner" "golang.org/x/xerrors" @@ -158,14 +161,20 @@ func (r *RootCmd) templatePush() *clibase.Cmd { var ( versionName string provisioner string - workdir string variablesFile string commandLineVariables []string alwaysPrompt bool provisionerTags []string uploadFlags templateUploadFlags activate bool - create bool + + requireActiveVersion bool + disableEveryone bool + defaultTTL time.Duration + failureTTL time.Duration + dormancyThreshold time.Duration + dormancyAutoDeletion time.Duration + maxTTL time.Duration ) client := new(codersdk.Client) cmd := &clibase.Cmd{ @@ -176,7 +185,18 @@ func (r *RootCmd) templatePush() *clibase.Cmd { r.InitClient(client), ), Handler: func(inv *clibase.Invocation) error { - uploadFlags.setWorkdir(workdir) + err := handleEntitlements(inv.Context(), handleEntitlementsArgs{ + client: client, + requireActiveVersion: requireActiveVersion, + defaultTTL: defaultTTL, + failureTTL: failureTTL, + dormancyThreshold: dormancyThreshold, + dormancyAutoDeletion: dormancyAutoDeletion, + maxTTL: maxTTL, + }) + if err != nil { + return err + } organization, err := CurrentOrganization(inv, client) if err != nil { @@ -188,10 +208,15 @@ func (r *RootCmd) templatePush() *clibase.Cmd { return err } + if utf8.RuneCountInString(name) > 31 { + return xerrors.Errorf("Template name must be less than 32 characters") + } + var createTemplate bool template, err := client.TemplateByName(inv.Context(), organization.ID, name) if err != nil { - if !create { + var apiError *codersdk.Error + if errors.As(err, &apiError) && apiError.StatusCode() != http.StatusNotFound { return err } createTemplate = true @@ -268,6 +293,48 @@ func (r *RootCmd) templatePush() *clibase.Cmd { } } + editTemplate := requireActiveVersion || + disableEveryone || + defaultTTL != 0 || + failureTTL != 0 || + dormancyThreshold != 0 || + dormancyAutoDeletion != 0 || + maxTTL != 0 + if editTemplate { + if defaultTTL == 0 { + defaultTTL = time.Duration(template.DefaultTTLMillis) * time.Millisecond + } + if failureTTL == 0 { + failureTTL = time.Duration(template.FailureTTLMillis) * time.Millisecond + } + if dormancyThreshold == 0 { + dormancyThreshold = time.Duration(template.TimeTilDormantMillis) * time.Millisecond + } + if dormancyAutoDeletion == 0 { + dormancyAutoDeletion = time.Duration(template.TimeTilDormantAutoDeleteMillis) * time.Millisecond + } + if maxTTL == 0 { + maxTTL = time.Duration(template.MaxTTLMillis) * time.Millisecond + } + req := codersdk.UpdateTemplateMeta{ + RequireActiveVersion: requireActiveVersion, + DisableEveryone: disableEveryone, + DefaultTTLMillis: defaultTTL.Milliseconds(), + FailureTTLMillis: failureTTL.Milliseconds(), + TimeTilDormantMillis: dormancyThreshold.Milliseconds(), + TimeTilDormantAutoDeleteMillis: dormancyAutoDeletion.Milliseconds(), + MaxTTLMillis: maxTTL.Milliseconds(), + } + + _, err = client.UpdateTemplateMeta(inv.Context(), template.ID, req) + if err != nil { + return xerrors.Errorf("update template metadata: %w", err) + } + if err != nil { + return err + } + } + _, _ = fmt.Fprintf(inv.Stdout, "Updated version at %s!\n", pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp))) return nil }, @@ -282,14 +349,6 @@ func (r *RootCmd) templatePush() *clibase.Cmd { // This is for testing! Hidden: true, }, - { - Flag: "test.workdir", - Description: "Customize the working directory.", - Default: "", - Value: clibase.StringOf(&workdir), - // This is for testing! - Hidden: true, - }, { Flag: "variables-file", Description: "Specify a file path with values for Terraform-managed variables.", @@ -327,10 +386,45 @@ func (r *RootCmd) templatePush() *clibase.Cmd { Value: clibase.BoolOf(&activate), }, { - Flag: "create", - Description: "Create the template if it does not exist.", + Flag: "require-active-version", + Description: "Requires workspace builds to use the active template version. This setting does not apply to template admins. This is an enterprise-only feature.", + Value: clibase.BoolOf(&requireActiveVersion), Default: "false", - Value: clibase.BoolOf(&create), + }, + { + Flag: "default-ttl", + Description: "Specify a default TTL for workspaces created from this template. It is the default time before shutdown - workspaces created from this template default to this value. Maps to \"Default autostop\" in the UI.", + Default: "24h", + Value: clibase.DurationOf(&defaultTTL), + }, + { + Flag: "failure-ttl", + Description: "Specify a failure TTL for workspaces created from this template. It is the amount of time after a failed \"start\" build before coder automatically schedules a \"stop\" build to cleanup.This licensed feature's default is 0h (off). Maps to \"Failure cleanup\"in the UI.", + Default: "0h", + Value: clibase.DurationOf(&failureTTL), + }, + { + Flag: "dormancy-threshold", + Description: "Specify a duration workspaces may be inactive prior to being moved to the dormant state. This licensed feature's default is 0h (off). Maps to \"Dormancy threshold\" in the UI.", + Default: "0h", + Value: clibase.DurationOf(&dormancyThreshold), + }, + { + Flag: "dormancy-auto-deletion", + Description: "Specify a duration workspaces may be in the dormant state prior to being deleted. This licensed feature's default is 0h (off). Maps to \"Dormancy Auto-Deletion\" in the UI.", + Default: "0h", + Value: clibase.DurationOf(&dormancyAutoDeletion), + }, + { + Flag: "max-ttl", + Description: "Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting. This is an enterprise-only feature.", + Value: clibase.DurationOf(&maxTTL), + }, + { + Flag: "private", + Description: "Disable the default behavior of granting template access to the 'everyone' group. " + + "The template permissions must be updated to allow non-admin users to use this template.", + Value: clibase.BoolOf(&disableEveryone), }, cliui.SkipPromptOption(), } From 675feb000ef8f6d15d9a79af979270f8238e3520 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 20 Dec 2023 13:49:20 +0000 Subject: [PATCH 02/19] add back work dir --- cli/templatepush.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cli/templatepush.go b/cli/templatepush.go index e06fb589e2c49..5ec040efb978f 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -161,6 +161,7 @@ func (r *RootCmd) templatePush() *clibase.Cmd { var ( versionName string provisioner string + workdir string variablesFile string commandLineVariables []string alwaysPrompt bool @@ -185,6 +186,8 @@ func (r *RootCmd) templatePush() *clibase.Cmd { r.InitClient(client), ), Handler: func(inv *clibase.Invocation) error { + uploadFlags.setWorkdir(workdir) + err := handleEntitlements(inv.Context(), handleEntitlementsArgs{ client: client, requireActiveVersion: requireActiveVersion, From 06618d7613702c71be12a28da16716341bb325bf Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 20 Dec 2023 14:10:04 +0000 Subject: [PATCH 03/19] add groupacl --- cli/templatepush.go | 2 +- coderd/database/queries.sql.go | 19 ++++---- coderd/database/queries/templates.sql | 3 +- coderd/templates.go | 6 +++ codersdk/templates.go | 6 +++ docs/cli/templates_push.md | 64 ++++++++++++++++++++++++--- site/src/api/typesGenerated.ts | 1 + 7 files changed, 85 insertions(+), 16 deletions(-) diff --git a/cli/templatepush.go b/cli/templatepush.go index 5ec040efb978f..7e8c5abbfb268 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -321,7 +321,7 @@ func (r *RootCmd) templatePush() *clibase.Cmd { } req := codersdk.UpdateTemplateMeta{ RequireActiveVersion: requireActiveVersion, - DisableEveryone: disableEveryone, + DisableEveryoneGroupAccess: disableEveryone, DefaultTTLMillis: defaultTTL.Milliseconds(), FailureTTLMillis: failureTTL.Milliseconds(), TimeTilDormantMillis: dormancyThreshold.Milliseconds(), diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 2a1f3b316c650..81bbe52386cf9 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6075,19 +6075,21 @@ SET name = $4, icon = $5, display_name = $6, - allow_user_cancel_workspace_jobs = $7 + allow_user_cancel_workspace_jobs = $7, + group_acl = $8 WHERE id = $1 ` type UpdateTemplateMetaByIDParams struct { - ID uuid.UUID `db:"id" json:"id"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` - Description string `db:"description" json:"description"` - Name string `db:"name" json:"name"` - Icon string `db:"icon" json:"icon"` - DisplayName string `db:"display_name" json:"display_name"` - AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"` + ID uuid.UUID `db:"id" json:"id"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Description string `db:"description" json:"description"` + Name string `db:"name" json:"name"` + Icon string `db:"icon" json:"icon"` + DisplayName string `db:"display_name" json:"display_name"` + AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"` + GroupACL TemplateACL `db:"group_acl" json:"group_acl"` } func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error { @@ -6099,6 +6101,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl arg.Icon, arg.DisplayName, arg.AllowUserCancelWorkspaceJobs, + arg.GroupACL, ) return err } diff --git a/coderd/database/queries/templates.sql b/coderd/database/queries/templates.sql index af8c3fe80f420..ca031bb0bd839 100644 --- a/coderd/database/queries/templates.sql +++ b/coderd/database/queries/templates.sql @@ -115,7 +115,8 @@ SET name = $4, icon = $5, display_name = $6, - allow_user_cancel_workspace_jobs = $7 + allow_user_cancel_workspace_jobs = $7, + group_acl = $8 WHERE id = $1 ; diff --git a/coderd/templates.go b/coderd/templates.go index 5e6d9644a782f..d4c33a454ce16 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -667,6 +667,11 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { name = template.Name } + groupACL := template.GroupACL + if req.DisableEveryoneGroupAccess { + groupACL = database.TemplateACL{} + } + var err error err = tx.UpdateTemplateMetaByID(ctx, database.UpdateTemplateMetaByIDParams{ ID: template.ID, @@ -676,6 +681,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) { Description: req.Description, Icon: req.Icon, AllowUserCancelWorkspaceJobs: req.AllowUserCancelWorkspaceJobs, + GroupACL: groupACL, }) if err != nil { return xerrors.Errorf("update template metadata: %w", err) diff --git a/codersdk/templates.go b/codersdk/templates.go index 8164843ad0c66..1be4d931ad7a2 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -241,6 +241,12 @@ type UpdateTemplateMeta struct { // If passed an empty string, will remove the deprecated message, making // the template usable for new workspaces again. DeprecationMessage *string `json:"deprecation_message"` + // DisableEveryoneGroupAccess allows optionally disabling the default + // behavior of granting the 'everyone' group access to use the template. + // If this is set to true, the template will not be available to all users, + // and must be explicitly granted to users or groups in the permissions settings + // of the template. + DisableEveryoneGroupAccess bool `json:"disable_everyone_group_access"` } type TemplateExample struct { diff --git a/docs/cli/templates_push.md b/docs/cli/templates_push.md index bfa73fdad1151..2c7b737e98840 100644 --- a/docs/cli/templates_push.md +++ b/docs/cli/templates_push.md @@ -29,14 +29,14 @@ Whether the new template will be marked active. Always prompt all parameters. Does not pull parameter values from active template version. -### --create +### --default-ttl -| | | -| ------- | ------------------ | -| Type | bool | -| Default | false | +| | | +| ------- | --------------------- | +| Type | duration | +| Default | 24h | -Create the template if it does not exist. +Specify a default TTL for workspaces created from this template. It is the default time before shutdown - workspaces created from this template default to this value. Maps to "Default autostop" in the UI. ### -d, --directory @@ -47,6 +47,33 @@ Create the template if it does not exist. Specify the directory to create from, use '-' to read tar from stdin. +### --dormancy-auto-deletion + +| | | +| ------- | --------------------- | +| Type | duration | +| Default | 0h | + +Specify a duration workspaces may be in the dormant state prior to being deleted. This licensed feature's default is 0h (off). Maps to "Dormancy Auto-Deletion" in the UI. + +### --dormancy-threshold + +| | | +| ------- | --------------------- | +| Type | duration | +| Default | 0h | + +Specify a duration workspaces may be inactive prior to being moved to the dormant state. This licensed feature's default is 0h (off). Maps to "Dormancy threshold" in the UI. + +### --failure-ttl + +| | | +| ------- | --------------------- | +| Type | duration | +| Default | 0h | + +Specify a failure TTL for workspaces created from this template. It is the amount of time after a failed "start" build before coder automatically schedules a "stop" build to cleanup.This licensed feature's default is 0h (off). Maps to "Failure cleanup"in the UI. + ### --ignore-lockfile | | | @@ -56,6 +83,14 @@ Specify the directory to create from, use '-' to read tar from stdin. Ignore warnings about not having a .terraform.lock.hcl file present in the template. +### --max-ttl + +| | | +| ---- | --------------------- | +| Type | duration | + +Edit the template maximum time before shutdown - workspaces created from this template must shutdown within the given duration after starting. This is an enterprise-only feature. + ### -m, --message | | | @@ -72,6 +107,14 @@ Specify a message describing the changes in this version of the template. Messag Specify a name for the new template version. It will be automatically generated if not provided. +### --private + +| | | +| ---- | ----------------- | +| Type | bool | + +Disable the default behavior of granting template access to the 'everyone' group. The template permissions must be updated to allow non-admin users to use this template. + ### --provisioner-tag | | | @@ -80,6 +123,15 @@ Specify a name for the new template version. It will be automatically generated Specify a set of tags to target provisioner daemons. +### --require-active-version + +| | | +| ------- | ------------------ | +| Type | bool | +| Default | false | + +Requires workspace builds to use the active template version. This setting does not apply to template admins. This is an enterprise-only feature. + ### --var | | | diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 98def777d9a91..bd5b35ecd5aab 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1264,6 +1264,7 @@ export interface UpdateTemplateMeta { readonly update_workspace_dormant_at: boolean; readonly require_active_version: boolean; readonly deprecation_message?: string; + readonly disable_everyone_group_access: boolean; } // From codersdk/users.go From 95d1a361d72d3449ead74ea13357a5023084ed6b Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 20 Dec 2023 14:11:37 +0000 Subject: [PATCH 04/19] Add back workdir --- cli/templatepush.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cli/templatepush.go b/cli/templatepush.go index 7e8c5abbfb268..faa38ff3122e5 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -352,6 +352,14 @@ func (r *RootCmd) templatePush() *clibase.Cmd { // This is for testing! Hidden: true, }, + { + Flag: "test.workdir", + Description: "Customize the working directory.", + Default: "", + Value: clibase.StringOf(&workdir), + // This is for testing! + Hidden: true, + }, { Flag: "variables-file", Description: "Specify a file path with values for Terraform-managed variables.", From 7dd63a8756a1ede06c1cfb608b513e4687258ed4 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 20 Dec 2023 18:33:02 +0000 Subject: [PATCH 05/19] combine edit flags --- cli/templatecreate.go | 6 +- cli/templateedit.go | 280 ++++++++++++++++++++++++++++++++---------- cli/templatepush.go | 184 ++++++++++++++++++++++----- 3 files changed, 376 insertions(+), 94 deletions(-) diff --git a/cli/templatecreate.go b/cli/templatecreate.go index 8cec28a187e00..c696dccf3a4ee 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -47,6 +47,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { r.InitClient(client), ), Handler: func(inv *clibase.Invocation) error { +<<<<<<< HEAD <<<<<<< HEAD isTemplateSchedulingOptionsSet := failureTTL != 0 || dormancyThreshold != 0 || dormancyAutoDeletion != 0 || maxTTL != 0 @@ -83,6 +84,9 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { } ======= err := handleEntitlements(inv.Context(), handleEntitlementsArgs{ +======= + err := createEntitlementsCheck(inv.Context(), handleEntitlementsArgs{ +>>>>>>> 3c377e5d3 (combine edit flags) client: client, requireActiveVersion: requireActiveVersion, defaultTTL: defaultTTL, @@ -383,7 +387,7 @@ type handleEntitlementsArgs struct { maxTTL time.Duration } -func handleEntitlements(ctx context.Context, args handleEntitlementsArgs) error { +func createEntitlementsCheck(ctx context.Context, args handleEntitlementsArgs) error { isTemplateSchedulingOptionsSet := args.failureTTL != 0 || args.dormancyThreshold != 0 || args.dormancyAutoDeletion != 0 || args.maxTTL != 0 if isTemplateSchedulingOptionsSet || args.requireActiveVersion { diff --git a/cli/templateedit.go b/cli/templateedit.go index 9cbcefc88730f..57fc35a2fbc64 100644 --- a/cli/templateedit.go +++ b/cli/templateedit.go @@ -1,6 +1,7 @@ package cli import ( + "context" "fmt" "net/http" "strings" @@ -46,6 +47,7 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { ), Short: "Edit the metadata of a template by name.", Handler: func(inv *clibase.Invocation) error { +<<<<<<< HEAD // This clause can be removed when workspace_actions is no longer experimental if failureTTL != 0 || dormancyThreshold != 0 || dormancyAutoDeletion != 0 { experiments, exErr := client.Experiments(inv.Context()) @@ -89,6 +91,8 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { } } +======= +>>>>>>> 3c377e5d3 (combine edit flags) organization, err := CurrentOrganization(inv, client) if err != nil { return xerrors.Errorf("get current organization: %w", err) @@ -98,71 +102,55 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { return xerrors.Errorf("get workspace template: %w", err) } - // Copy the default value if the list is empty, or if the user - // specified the "none" value clear the list. - if len(autostopRequirementDaysOfWeek) == 0 { - autostopRequirementDaysOfWeek = template.AutostopRequirement.DaysOfWeek - } - if len(autostartRequirementDaysOfWeek) == 1 && autostartRequirementDaysOfWeek[0] == "all" { - // Set it to every day of the week - autostartRequirementDaysOfWeek = []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"} - } else if len(autostartRequirementDaysOfWeek) == 0 { - autostartRequirementDaysOfWeek = template.AutostartRequirement.DaysOfWeek - } - if unsetAutostopRequirementDaysOfWeek { - autostopRequirementDaysOfWeek = []string{} - } - if failureTTL == 0 { - failureTTL = time.Duration(template.FailureTTLMillis) * time.Millisecond - } - if dormancyThreshold == 0 { - dormancyThreshold = time.Duration(template.TimeTilDormantMillis) * time.Millisecond - } - if dormancyAutoDeletion == 0 { - dormancyAutoDeletion = time.Duration(template.TimeTilDormantAutoDeleteMillis) * time.Millisecond - } - - // Default values - if !userSetOption(inv, "description") { - description = template.Description - } + unsetAutostopRequirementDaysOfWeek, err := editTemplateEntitlementsCheck(inv.Context(), editTemplateEntitlementsArgs{ + client: client, + inv: inv, + template: template, - if !userSetOption(inv, "icon") { - icon = template.Icon - } + name: name, + displayName: displayName, + description: description, + icon: icon, + defaultTTL: defaultTTL, + maxTTL: maxTTL, + autostopRequirementDaysOfWeek: autostopRequirementDaysOfWeek, + autostopRequirementWeeks: autostopRequirementWeeks, + autostartRequirementDaysOfWeek: autostartRequirementDaysOfWeek, + failureTTL: failureTTL, + dormancyThreshold: dormancyThreshold, + dormancyAutoDeletion: dormancyAutoDeletion, + allowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs, + allowUserAutostart: allowUserAutostart, + allowUserAutostop: allowUserAutostop, + requireActiveVersion: requireActiveVersion, + deprecationMessage: deprecationMessage, + disableEveryone: false, // TODO: Add new flag + }) - if !userSetOption(inv, "display-name") { - displayName = template.DisplayName - } + req := updateTemplateMetaRequest(updateTemplateMetaArgs{ + client: client, + inv: inv, + template: template, + unsetAutostopRequirementDaysOfWeek: unsetAutostopRequirementDaysOfWeek, - var deprecated *string - if !userSetOption(inv, "deprecated") { - deprecated = &deprecationMessage - } - - req := codersdk.UpdateTemplateMeta{ - Name: name, - DisplayName: displayName, - Description: description, - Icon: icon, - DefaultTTLMillis: defaultTTL.Milliseconds(), - MaxTTLMillis: maxTTL.Milliseconds(), - AutostopRequirement: &codersdk.TemplateAutostopRequirement{ - DaysOfWeek: autostopRequirementDaysOfWeek, - Weeks: autostopRequirementWeeks, - }, - AutostartRequirement: &codersdk.TemplateAutostartRequirement{ - DaysOfWeek: autostartRequirementDaysOfWeek, - }, - FailureTTLMillis: failureTTL.Milliseconds(), - TimeTilDormantMillis: dormancyThreshold.Milliseconds(), - TimeTilDormantAutoDeleteMillis: dormancyAutoDeletion.Milliseconds(), - AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs, - AllowUserAutostart: allowUserAutostart, - AllowUserAutostop: allowUserAutostop, - RequireActiveVersion: requireActiveVersion, - DeprecationMessage: deprecated, - } + name: name, + displayName: displayName, + description: description, + icon: icon, + defaultTTL: defaultTTL, + maxTTL: maxTTL, + autostopRequirementDaysOfWeek: autostopRequirementDaysOfWeek, + autostopRequirementWeeks: autostopRequirementWeeks, + autostartRequirementDaysOfWeek: autostartRequirementDaysOfWeek, + failureTTL: failureTTL, + dormancyThreshold: dormancyThreshold, + dormancyAutoDeletion: dormancyAutoDeletion, + allowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs, + allowUserAutostart: allowUserAutostart, + allowUserAutostop: allowUserAutostop, + requireActiveVersion: requireActiveVersion, + deprecationMessage: deprecationMessage, + }) _, err = client.UpdateTemplateMeta(inv.Context(), template.ID, req) if err != nil { @@ -297,3 +285,171 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { return cmd } + +type editTemplateEntitlementsArgs struct { + client *codersdk.Client + inv *clibase.Invocation + + defaultTTL time.Duration + maxTTL time.Duration + autostopRequirementDaysOfWeek []string + autostopRequirementWeeks int64 + autostartRequirementDaysOfWeek []string + failureTTL time.Duration + dormancyThreshold time.Duration + dormancyAutoDeletion time.Duration + allowUserCancelWorkspaceJobs bool + allowUserAutostart bool + allowUserAutostop bool + requireActiveVersion bool +} + +func editTemplateEntitlementsCheck(ctx context.Context, args editTemplateEntitlementsArgs) (bool, error) { + // This clause can be removed when workspace_actions is no longer experimental + if args.failureTTL != 0 || args.dormancyThreshold != 0 || args.dormancyAutoDeletion != 0 { + experiments, exErr := args.client.Experiments(ctx) + if exErr != nil { + return false, xerrors.Errorf("get experiments: %w", exErr) + } + + if !experiments.Enabled(codersdk.ExperimentWorkspaceActions) { + return false, xerrors.Errorf("--failure-ttl, --dormancy-threshold, and --dormancy-auto-deletion are experimental features. Use the workspace_actions CODER_EXPERIMENTS flag to set these configuration values.") + } + } + + unsetAutostopRequirementDaysOfWeek := len(args.autostopRequirementDaysOfWeek) == 1 && args.autostopRequirementDaysOfWeek[0] == "none" + requiresScheduling := (len(args.autostopRequirementDaysOfWeek) > 0 && !unsetAutostopRequirementDaysOfWeek) || + args.autostopRequirementWeeks > 0 || + !args.allowUserAutostart || + !args.allowUserAutostop || + args.maxTTL != 0 || + args.failureTTL != 0 || + args.dormancyThreshold != 0 || + args.dormancyAutoDeletion != 0 || + len(args.autostartRequirementDaysOfWeek) > 0 + + requiresEntitlement := requiresScheduling || args.requireActiveVersion + if requiresEntitlement { + entitlements, err := args.client.Entitlements(ctx) + if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusNotFound { + return false, xerrors.Errorf("your deployment appears to be an AGPL deployment, so you cannot set enterprise-only flags") + } else if err != nil { + return false, xerrors.Errorf("get entitlements: %w", err) + } + + if requiresScheduling && !entitlements.Features[codersdk.FeatureAdvancedTemplateScheduling].Enabled { + return false, xerrors.Errorf("your license is not entitled to use advanced template scheduling, so you cannot set --max-ttl, --failure-ttl, --inactivityTTL, --allow-user-autostart=false or --allow-user-autostop=false") + } + + if args.requireActiveVersion { + if !entitlements.Features[codersdk.FeatureAccessControl].Enabled { + return false, xerrors.Errorf("your license is not entitled to use enterprise access control, so you cannot set --require-active-version") + } + + experiments, exErr := args.client.Experiments(ctx) + if exErr != nil { + return false, xerrors.Errorf("get experiments: %w", exErr) + } + + if !experiments.Enabled(codersdk.ExperimentTemplateUpdatePolicies) { + return false, xerrors.Errorf("--require-active-version is an experimental feature, contact an administrator to enable the 'template_update_policies' experiment on your Coder server") + } + } + } + + return unsetAutostopRequirementDaysOfWeek, nil +} + +type updateTemplateMetaArgs struct { + client *codersdk.Client + inv *clibase.Invocation + template codersdk.Template + unsetAutostopRequirementDaysOfWeek bool + + name string + displayName string + description string + icon string + defaultTTL time.Duration + maxTTL time.Duration + autostopRequirementDaysOfWeek []string + autostopRequirementWeeks int64 + autostartRequirementDaysOfWeek []string + failureTTL time.Duration + dormancyThreshold time.Duration + dormancyAutoDeletion time.Duration + allowUserCancelWorkspaceJobs bool + allowUserAutostart bool + allowUserAutostop bool + requireActiveVersion bool + deprecationMessage string + disableEveryone bool +} + +func updateTemplateMetaRequest(args updateTemplateMetaArgs) codersdk.UpdateTemplateMeta { + // Copy the default value if the list is empty, or if the user + // specified the "none" value clear the list. + if len(args.autostopRequirementDaysOfWeek) == 0 { + args.autostopRequirementDaysOfWeek = args.template.AutostopRequirement.DaysOfWeek + } + if len(args.autostartRequirementDaysOfWeek) == 1 && args.autostartRequirementDaysOfWeek[0] == "all" { + // Set it to every day of the week + args.autostartRequirementDaysOfWeek = []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"} + } else if len(args.autostartRequirementDaysOfWeek) == 0 { + args.autostartRequirementDaysOfWeek = args.template.AutostartRequirement.DaysOfWeek + } + if args.unsetAutostopRequirementDaysOfWeek { + args.autostopRequirementDaysOfWeek = []string{} + } + if args.failureTTL == 0 { + args.failureTTL = time.Duration(args.template.FailureTTLMillis) * time.Millisecond + } + if args.dormancyThreshold == 0 { + args.dormancyThreshold = time.Duration(args.template.TimeTilDormantMillis) * time.Millisecond + } + if args.dormancyAutoDeletion == 0 { + args.dormancyAutoDeletion = time.Duration(args.template.TimeTilDormantAutoDeleteMillis) * time.Millisecond + } + + // Default values + if !userSetOption(args.inv, "description") { + args.description = args.template.Description + } + + if !userSetOption(args.inv, "icon") { + args.icon = args.template.Icon + } + + if !userSetOption(args.inv, "display-name") { + args.displayName = args.template.DisplayName + } + + var deprecated *string + if !userSetOption(args.inv, "deprecated") { + deprecated = &args.deprecationMessage + } + + return codersdk.UpdateTemplateMeta{ + Name: args.name, + DisplayName: args.displayName, + Description: args.description, + Icon: args.icon, + DefaultTTLMillis: args.defaultTTL.Milliseconds(), + MaxTTLMillis: args.maxTTL.Milliseconds(), + AutostopRequirement: &codersdk.TemplateAutostopRequirement{ + DaysOfWeek: args.autostopRequirementDaysOfWeek, + Weeks: args.autostopRequirementWeeks, + }, + AutostartRequirement: &codersdk.TemplateAutostartRequirement{ + DaysOfWeek: args.autostartRequirementDaysOfWeek, + }, + FailureTTLMillis: args.failureTTL.Milliseconds(), + TimeTilDormantMillis: args.dormancyThreshold.Milliseconds(), + TimeTilDormantAutoDeleteMillis: args.dormancyAutoDeletion.Milliseconds(), + AllowUserCancelWorkspaceJobs: args.allowUserCancelWorkspaceJobs, + AllowUserAutostart: args.allowUserAutostart, + AllowUserAutostop: args.allowUserAutostop, + RequireActiveVersion: args.requireActiveVersion, + DeprecationMessage: deprecated, + } +} diff --git a/cli/templatepush.go b/cli/templatepush.go index faa38ff3122e5..f17e0c3982647 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -169,13 +169,23 @@ func (r *RootCmd) templatePush() *clibase.Cmd { uploadFlags templateUploadFlags activate bool - requireActiveVersion bool - disableEveryone bool - defaultTTL time.Duration - failureTTL time.Duration - dormancyThreshold time.Duration - dormancyAutoDeletion time.Duration - maxTTL time.Duration + displayName string + description string + icon string + requireActiveVersion bool + disableEveryone bool + defaultTTL time.Duration + failureTTL time.Duration + dormancyThreshold time.Duration + dormancyAutoDeletion time.Duration + maxTTL time.Duration + autostopRequirementDaysOfWeek []string + autostopRequirementWeeks int64 + autostartRequirementDaysOfWeek []string + allowUserAutostart bool + allowUserAutostop bool + allowUserCancelWorkspaceJobs bool + deprecationMessage string ) client := new(codersdk.Client) cmd := &clibase.Cmd{ @@ -188,7 +198,7 @@ func (r *RootCmd) templatePush() *clibase.Cmd { Handler: func(inv *clibase.Invocation) error { uploadFlags.setWorkdir(workdir) - err := handleEntitlements(inv.Context(), handleEntitlementsArgs{ + err := createEntitlementsCheck(inv.Context(), handleEntitlementsArgs{ client: client, requireActiveVersion: requireActiveVersion, defaultTTL: defaultTTL, @@ -201,6 +211,23 @@ func (r *RootCmd) templatePush() *clibase.Cmd { return err } + unsetAutostopRequirementDaysOfWeek, err := editTemplateEntitlementsCheck(inv.Context(), editTemplateEntitlementsArgs{ + client: client, + inv: inv, + defaultTTL: defaultTTL, + maxTTL: maxTTL, + autostopRequirementDaysOfWeek: autostopRequirementDaysOfWeek, + autostopRequirementWeeks: autostopRequirementWeeks, + autostartRequirementDaysOfWeek: autostartRequirementDaysOfWeek, + failureTTL: failureTTL, + dormancyThreshold: dormancyThreshold, + dormancyAutoDeletion: dormancyAutoDeletion, + allowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs, + allowUserAutostart: allowUserAutostart, + allowUserAutostop: allowUserAutostop, + requireActiveVersion: requireActiveVersion, + }) + organization, err := CurrentOrganization(inv, client) if err != nil { return err @@ -304,30 +331,34 @@ func (r *RootCmd) templatePush() *clibase.Cmd { dormancyAutoDeletion != 0 || maxTTL != 0 if editTemplate { - if defaultTTL == 0 { - defaultTTL = time.Duration(template.DefaultTTLMillis) * time.Millisecond - } - if failureTTL == 0 { - failureTTL = time.Duration(template.FailureTTLMillis) * time.Millisecond - } - if dormancyThreshold == 0 { - dormancyThreshold = time.Duration(template.TimeTilDormantMillis) * time.Millisecond - } - if dormancyAutoDeletion == 0 { - dormancyAutoDeletion = time.Duration(template.TimeTilDormantAutoDeleteMillis) * time.Millisecond - } - if maxTTL == 0 { - maxTTL = time.Duration(template.MaxTTLMillis) * time.Millisecond - } - req := codersdk.UpdateTemplateMeta{ - RequireActiveVersion: requireActiveVersion, - DisableEveryoneGroupAccess: disableEveryone, - DefaultTTLMillis: defaultTTL.Milliseconds(), - FailureTTLMillis: failureTTL.Milliseconds(), - TimeTilDormantMillis: dormancyThreshold.Milliseconds(), - TimeTilDormantAutoDeleteMillis: dormancyAutoDeletion.Milliseconds(), - MaxTTLMillis: maxTTL.Milliseconds(), + template, err := client.TemplateByName(inv.Context(), organization.ID, name) + if err != nil { + return err } + req := updateTemplateMetaRequest(updateTemplateMetaArgs{ + client: client, + inv: inv, + template: template, + unsetAutostopRequirementDaysOfWeek: unsetAutostopRequirementDaysOfWeek, + + displayName: displayName, + description: description, + icon: icon, + requireActiveVersion: requireActiveVersion, + disableEveryone: disableEveryone, + defaultTTL: defaultTTL, + failureTTL: failureTTL, + dormancyThreshold: dormancyThreshold, + dormancyAutoDeletion: dormancyAutoDeletion, + maxTTL: maxTTL, + autostopRequirementDaysOfWeek: autostopRequirementDaysOfWeek, + autostopRequirementWeeks: autostopRequirementWeeks, + autostartRequirementDaysOfWeek: autostartRequirementDaysOfWeek, + allowUserAutostart: allowUserAutostart, + allowUserAutostop: allowUserAutostop, + allowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs, + deprecationMessage: deprecationMessage, + }) _, err = client.UpdateTemplateMeta(inv.Context(), template.ID, req) if err != nil { @@ -385,6 +416,27 @@ func (r *RootCmd) templatePush() *clibase.Cmd { Description: "Specify a name for the new template version. It will be automatically generated if not provided.", Value: clibase.StringOf(&versionName), }, + { + Flag: "display-name", + Description: "Edit the template display name.", + Value: clibase.StringOf(&displayName), + }, + { + Flag: "description", + Description: "Edit the template description.", + Value: clibase.StringOf(&description), + }, + { + Name: "Deprecated", + Flag: "deprecated", + Description: "Sets the template as deprecated. Must be a message explaining why the template is deprecated.", + Value: clibase.StringOf(&deprecationMessage), + }, + { + Flag: "icon", + Description: "Edit the template icon path.", + Value: clibase.StringOf(&icon), + }, { Flag: "always-prompt", Description: "Always prompt all parameters. Does not pull parameter values from active template version.", @@ -437,6 +489,76 @@ func (r *RootCmd) templatePush() *clibase.Cmd { "The template permissions must be updated to allow non-admin users to use this template.", Value: clibase.BoolOf(&disableEveryone), }, + { + Flag: "autostart-requirement-weekdays", + // workspaces created from this template must be restarted on the given weekdays. To unset this value for the template (and disable the autostop requirement for the template), pass 'none'. + Description: "Edit the template autostart requirement weekdays - workspaces created from this template can only autostart on the given weekdays. To unset this value for the template (and allow autostart on all days), pass 'all'.", + Value: clibase.Validate(clibase.StringArrayOf(&autostartRequirementDaysOfWeek), func(value *clibase.StringArray) error { + v := value.GetSlice() + if len(v) == 1 && v[0] == "all" { + return nil + } + _, err := codersdk.WeekdaysToBitmap(v) + if err != nil { + return xerrors.Errorf("invalid autostart requirement days of week %q: %w", strings.Join(v, ","), err) + } + return nil + }), + }, + { + Flag: "autostop-requirement-weekdays", + Description: "Edit the template autostop requirement weekdays - workspaces created from this template must be restarted on the given weekdays. To unset this value for the template (and disable the autostop requirement for the template), pass 'none'.", + // TODO(@dean): unhide when we delete max_ttl + Hidden: true, + Value: clibase.Validate(clibase.StringArrayOf(&autostopRequirementDaysOfWeek), func(value *clibase.StringArray) error { + v := value.GetSlice() + if len(v) == 1 && v[0] == "none" { + return nil + } + _, err := codersdk.WeekdaysToBitmap(v) + if err != nil { + return xerrors.Errorf("invalid autostop requirement days of week %q: %w", strings.Join(v, ","), err) + } + return nil + }), + }, + { + Flag: "autostop-requirement-weeks", + Description: "Edit the template autostop requirement weeks - workspaces created from this template must be restarted on an n-weekly basis.", + // TODO(@dean): unhide when we delete max_ttl + Hidden: true, + Value: clibase.Int64Of(&autostopRequirementWeeks), + }, + { + Flag: "dormancy-threshold", + Description: "Specify a duration workspaces may be inactive prior to being moved to the dormant state. This licensed feature's default is 0h (off). Maps to \"Dormancy threshold\" in the UI.", + Default: "0h", + Value: clibase.DurationOf(&dormancyThreshold), + }, + { + Flag: "dormancy-auto-deletion", + Description: "Specify a duration workspaces may be in the dormant state prior to being deleted. This licensed feature's default is 0h (off). Maps to \"Dormancy Auto-Deletion\" in the UI.", + Default: "0h", + Value: clibase.DurationOf(&dormancyAutoDeletion), + }, + { + Flag: "allow-user-cancel-workspace-jobs", + Description: "Allow users to cancel in-progress workspace jobs.", + Default: "true", + Value: clibase.BoolOf(&allowUserCancelWorkspaceJobs), + }, + { + Flag: "allow-user-autostart", + Description: "Allow users to configure autostart for workspaces on this template. This can only be disabled in enterprise.", + Default: "true", + Value: clibase.BoolOf(&allowUserAutostart), + }, + { + Flag: "allow-user-autostop", + Description: "Allow users to customize the autostop TTL for workspaces on this template. This can only be disabled in enterprise.", + Default: "true", + Value: clibase.BoolOf(&allowUserAutostop), + }, cliui.SkipPromptOption(), } cmd.Options = append(cmd.Options, uploadFlags.options()...) From 4443322527e5f7ae12d0328d0be55c5eaccdb47a Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 20 Dec 2023 18:33:48 +0000 Subject: [PATCH 06/19] fix edit --- cli/templateedit.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/cli/templateedit.go b/cli/templateedit.go index 57fc35a2fbc64..6558a74020354 100644 --- a/cli/templateedit.go +++ b/cli/templateedit.go @@ -103,14 +103,9 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { } unsetAutostopRequirementDaysOfWeek, err := editTemplateEntitlementsCheck(inv.Context(), editTemplateEntitlementsArgs{ - client: client, - inv: inv, - template: template, + client: client, + inv: inv, - name: name, - displayName: displayName, - description: description, - icon: icon, defaultTTL: defaultTTL, maxTTL: maxTTL, autostopRequirementDaysOfWeek: autostopRequirementDaysOfWeek, @@ -123,8 +118,6 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { allowUserAutostart: allowUserAutostart, allowUserAutostop: allowUserAutostop, requireActiveVersion: requireActiveVersion, - deprecationMessage: deprecationMessage, - disableEveryone: false, // TODO: Add new flag }) req := updateTemplateMetaRequest(updateTemplateMetaArgs{ From 82b143becacfe2b504f52852e810b9a51868406e Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 20 Dec 2023 20:50:52 +0000 Subject: [PATCH 07/19] unify edit and push --- cli/templatepush.go | 85 ++++++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/cli/templatepush.go b/cli/templatepush.go index f17e0c3982647..ce3c3058fed5f 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -227,6 +227,9 @@ func (r *RootCmd) templatePush() *clibase.Cmd { allowUserAutostop: allowUserAutostop, requireActiveVersion: requireActiveVersion, }) + if err != nil { + return err + } organization, err := CurrentOrganization(inv, client) if err != nil { @@ -323,53 +326,47 @@ func (r *RootCmd) templatePush() *clibase.Cmd { } } - editTemplate := requireActiveVersion || - disableEveryone || - defaultTTL != 0 || - failureTTL != 0 || - dormancyThreshold != 0 || - dormancyAutoDeletion != 0 || - maxTTL != 0 - if editTemplate { - template, err := client.TemplateByName(inv.Context(), organization.ID, name) - if err != nil { - return err - } - req := updateTemplateMetaRequest(updateTemplateMetaArgs{ - client: client, - inv: inv, - template: template, - unsetAutostopRequirementDaysOfWeek: unsetAutostopRequirementDaysOfWeek, - - displayName: displayName, - description: description, - icon: icon, - requireActiveVersion: requireActiveVersion, - disableEveryone: disableEveryone, - defaultTTL: defaultTTL, - failureTTL: failureTTL, - dormancyThreshold: dormancyThreshold, - dormancyAutoDeletion: dormancyAutoDeletion, - maxTTL: maxTTL, - autostopRequirementDaysOfWeek: autostopRequirementDaysOfWeek, - autostopRequirementWeeks: autostopRequirementWeeks, - autostartRequirementDaysOfWeek: autostartRequirementDaysOfWeek, - allowUserAutostart: allowUserAutostart, - allowUserAutostop: allowUserAutostop, - allowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs, - deprecationMessage: deprecationMessage, - }) + _, _ = fmt.Fprintf(inv.Stdout, "Updated version at %s!\n", pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp))) - _, err = client.UpdateTemplateMeta(inv.Context(), template.ID, req) - if err != nil { - return xerrors.Errorf("update template metadata: %w", err) - } - if err != nil { - return err - } + // refresh template data for edit api call + template, err = client.TemplateByName(inv.Context(), organization.ID, name) + if err != nil { + return err } + req := updateTemplateMetaRequest(updateTemplateMetaArgs{ + client: client, + inv: inv, + template: template, + unsetAutostopRequirementDaysOfWeek: unsetAutostopRequirementDaysOfWeek, + + displayName: displayName, + description: description, + icon: icon, + requireActiveVersion: requireActiveVersion, + disableEveryone: disableEveryone, + defaultTTL: defaultTTL, + failureTTL: failureTTL, + dormancyThreshold: dormancyThreshold, + dormancyAutoDeletion: dormancyAutoDeletion, + maxTTL: maxTTL, + autostopRequirementDaysOfWeek: autostopRequirementDaysOfWeek, + autostopRequirementWeeks: autostopRequirementWeeks, + autostartRequirementDaysOfWeek: autostartRequirementDaysOfWeek, + allowUserAutostart: allowUserAutostart, + allowUserAutostop: allowUserAutostop, + allowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs, + deprecationMessage: deprecationMessage, + }) + + _, err = client.UpdateTemplateMeta(inv.Context(), template.ID, req) + if err != nil { + return xerrors.Errorf("update template metadata: %w", err) + } + if err != nil { + return err + } + _, _ = fmt.Fprintf(inv.Stdout, "Updated template metadata at %s!\n", pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp))) - _, _ = fmt.Fprintf(inv.Stdout, "Updated version at %s!\n", pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp))) return nil }, } From 19a4dfcc7e76d2350009ccb32fb7ec4c83c7b081 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 20 Dec 2023 20:54:09 +0000 Subject: [PATCH 08/19] make gen --- docs/cli/templates_push.md | 85 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/docs/cli/templates_push.md b/docs/cli/templates_push.md index 2c7b737e98840..398e67206160f 100644 --- a/docs/cli/templates_push.md +++ b/docs/cli/templates_push.md @@ -12,6 +12,14 @@ coder templates push [flags] [template] ## Options +### --deprecated + +| | | +| ---- | ------------------- | +| Type | string | + +Sets the template as deprecated. Must be a message explaining why the template is deprecated. + ### --activate | | | @@ -21,6 +29,33 @@ coder templates push [flags] [template] Whether the new template will be marked active. +### --allow-user-autostart + +| | | +| ------- | ----------------- | +| Type | bool | +| Default | true | + +Allow users to configure autostart for workspaces on this template. This can only be disabled in enterprise. + +### --allow-user-autostop + +| | | +| ------- | ----------------- | +| Type | bool | +| Default | true | + +Allow users to customize the autostop TTL for workspaces on this template. This can only be disabled in enterprise. + +### --allow-user-cancel-workspace-jobs + +| | | +| ------- | ----------------- | +| Type | bool | +| Default | true | + +Allow users to cancel in-progress workspace jobs. + ### --always-prompt | | | @@ -29,6 +64,14 @@ Whether the new template will be marked active. Always prompt all parameters. Does not pull parameter values from active template version. +### --autostart-requirement-weekdays + +| | | +| ---- | ------------------------- | +| Type | string-array | + +Edit the template autostart requirement weekdays - workspaces created from this template can only autostart on the given weekdays. To unset this value for the template (and allow autostart on all days), pass 'all'. + ### --default-ttl | | | @@ -38,6 +81,14 @@ Always prompt all parameters. Does not pull parameter values from active templat Specify a default TTL for workspaces created from this template. It is the default time before shutdown - workspaces created from this template default to this value. Maps to "Default autostop" in the UI. +### --description + +| | | +| ---- | ------------------- | +| Type | string | + +Edit the template description. + ### -d, --directory | | | @@ -47,6 +98,23 @@ Specify a default TTL for workspaces created from this template. It is the defau Specify the directory to create from, use '-' to read tar from stdin. +### --display-name + +| | | +| ---- | ------------------- | +| Type | string | + +Edit the template display name. + +### --dormancy-auto-deletion + +| | | +| ------- | --------------------- | +| Type | duration | +| Default | 0h | + +Specify a duration workspaces may be in the dormant state prior to being deleted. This licensed feature's default is 0h (off). Maps to "Dormancy Auto-Deletion" in the UI. + ### --dormancy-auto-deletion | | | @@ -65,6 +133,15 @@ Specify a duration workspaces may be in the dormant state prior to being deleted Specify a duration workspaces may be inactive prior to being moved to the dormant state. This licensed feature's default is 0h (off). Maps to "Dormancy threshold" in the UI. +### --dormancy-threshold + +| | | +| ------- | --------------------- | +| Type | duration | +| Default | 0h | + +Specify a duration workspaces may be inactive prior to being moved to the dormant state. This licensed feature's default is 0h (off). Maps to "Dormancy threshold" in the UI. + ### --failure-ttl | | | @@ -74,6 +151,14 @@ Specify a duration workspaces may be inactive prior to being moved to the dorman Specify a failure TTL for workspaces created from this template. It is the amount of time after a failed "start" build before coder automatically schedules a "stop" build to cleanup.This licensed feature's default is 0h (off). Maps to "Failure cleanup"in the UI. +### --icon + +| | | +| ---- | ------------------- | +| Type | string | + +Edit the template icon path. + ### --ignore-lockfile | | | From 5a2aa6a7091cc540c2579487356c68367b79d169 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 21 Dec 2023 16:05:38 +0000 Subject: [PATCH 09/19] fix test --- cli/templateedit.go | 3 +++ cli/templatepush.go | 12 ------------ 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/cli/templateedit.go b/cli/templateedit.go index 6558a74020354..df965d4b05937 100644 --- a/cli/templateedit.go +++ b/cli/templateedit.go @@ -119,6 +119,9 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { allowUserAutostop: allowUserAutostop, requireActiveVersion: requireActiveVersion, }) + if err != nil { + return err + } req := updateTemplateMetaRequest(updateTemplateMetaArgs{ client: client, diff --git a/cli/templatepush.go b/cli/templatepush.go index ce3c3058fed5f..218bb062b6168 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -526,18 +526,6 @@ func (r *RootCmd) templatePush() *clibase.Cmd { Hidden: true, Value: clibase.Int64Of(&autostopRequirementWeeks), }, - { - Flag: "dormancy-threshold", - Description: "Specify a duration workspaces may be inactive prior to being moved to the dormant state. This licensed feature's default is 0h (off). Maps to \"Dormancy threshold\" in the UI.", - Default: "0h", - Value: clibase.DurationOf(&dormancyThreshold), - }, - { - Flag: "dormancy-auto-deletion", - Description: "Specify a duration workspaces may be in the dormant state prior to being deleted. This licensed feature's default is 0h (off). Maps to \"Dormancy Auto-Deletion\" in the UI.", - Default: "0h", - Value: clibase.DurationOf(&dormancyAutoDeletion), - }, { Flag: "allow-user-cancel-workspace-jobs", Description: "Allow users to cancel in-progress workspace jobs.", From 4ce4ddbffe4e378c9ca053a467f3f1bd4c0d7242 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 21 Dec 2023 16:20:03 +0000 Subject: [PATCH 10/19] make gen --- docs/cli/templates_push.md | 18 ------------------ .../TemplateSettingsForm.tsx | 1 + .../TemplateSettingsPage.test.tsx | 1 + .../TemplateScheduleForm.tsx | 2 ++ .../TemplateSchedulePage.test.tsx | 1 + 5 files changed, 5 insertions(+), 18 deletions(-) diff --git a/docs/cli/templates_push.md b/docs/cli/templates_push.md index 398e67206160f..9df46fdf69e9c 100644 --- a/docs/cli/templates_push.md +++ b/docs/cli/templates_push.md @@ -115,24 +115,6 @@ Edit the template display name. Specify a duration workspaces may be in the dormant state prior to being deleted. This licensed feature's default is 0h (off). Maps to "Dormancy Auto-Deletion" in the UI. -### --dormancy-auto-deletion - -| | | -| ------- | --------------------- | -| Type | duration | -| Default | 0h | - -Specify a duration workspaces may be in the dormant state prior to being deleted. This licensed feature's default is 0h (off). Maps to "Dormancy Auto-Deletion" in the UI. - -### --dormancy-threshold - -| | | -| ------- | --------------------- | -| Type | duration | -| Default | 0h | - -Specify a duration workspaces may be inactive prior to being moved to the dormant state. This licensed feature's default is 0h (off). Maps to "Dormancy threshold" in the UI. - ### --dormancy-threshold | | | diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx index 2c37ca5729ba6..77de9c36bb583 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx @@ -77,6 +77,7 @@ export const TemplateSettingsForm: FC = ({ update_workspace_dormant_at: false, require_active_version: template.require_active_version, deprecation_message: template.deprecation_message, + disable_everyone_group_access: false, }, validationSchema, onSubmit, diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx index be0d593c9e13e..ee6153646584a 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx @@ -47,6 +47,7 @@ const validFormValues: FormValues = { update_workspace_last_used_at: false, update_workspace_dormant_at: false, require_active_version: false, + disable_everyone_group_access: false, }; const renderTemplateSettingsPage = async () => { diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx index f1f0af511ec9b..89f26cc5d451e 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx @@ -118,6 +118,7 @@ export const TemplateScheduleForm: FC = ({ update_workspace_last_used_at: false, update_workspace_dormant_at: false, require_active_version: false, + disable_everyone_group_access: false, }, validationSchema, onSubmit: () => { @@ -238,6 +239,7 @@ export const TemplateScheduleForm: FC = ({ update_workspace_last_used_at: form.values.update_workspace_last_used_at, update_workspace_dormant_at: form.values.update_workspace_dormant_at, require_active_version: false, + disable_everyone_group_access: false, }); }; diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx index 77e50d73f0657..1a5618cf3263a 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx @@ -28,6 +28,7 @@ const validFormValues: TemplateScheduleFormValues = { inactivity_cleanup_enabled: false, dormant_autodeletion_cleanup_enabled: false, require_active_version: false, + disable_everyone_group_access: false, autostart_requirement_days_of_week: [ "monday", "tuesday", From 39ed2cd949fdb8dfda5fc88c41743134dfad37a9 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 21 Dec 2023 16:36:07 +0000 Subject: [PATCH 11/19] update golden --- .../coder_templates_push_--help.golden | 67 ++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/cli/testdata/coder_templates_push_--help.golden b/cli/testdata/coder_templates_push_--help.golden index 9d255c1f8bc23..e484c143e9971 100644 --- a/cli/testdata/coder_templates_push_--help.golden +++ b/cli/testdata/coder_templates_push_--help.golden @@ -6,23 +6,76 @@ USAGE: Push a new template version from the current directory or as specified by flag OPTIONS: + --deprecated string + Sets the template as deprecated. Must be a message explaining why the + template is deprecated. + --activate bool (default: true) Whether the new template will be marked active. + --allow-user-autostart bool (default: true) + Allow users to configure autostart for workspaces on this template. + This can only be disabled in enterprise. + + --allow-user-autostop bool (default: true) + Allow users to customize the autostop TTL for workspaces on this + template. This can only be disabled in enterprise. + + --allow-user-cancel-workspace-jobs bool (default: true) + Allow users to cancel in-progress workspace jobs. + --always-prompt bool Always prompt all parameters. Does not pull parameter values from active template version. - --create bool (default: false) - Create the template if it does not exist. + --autostart-requirement-weekdays string-array + Edit the template autostart requirement weekdays - workspaces created + from this template can only autostart on the given weekdays. To unset + this value for the template (and allow autostart on all days), pass + 'all'. + + --default-ttl duration (default: 24h) + Specify a default TTL for workspaces created from this template. It is + the default time before shutdown - workspaces created from this + template default to this value. Maps to "Default autostop" in the UI. + + --description string + Edit the template description. -d, --directory string (default: .) Specify the directory to create from, use '-' to read tar from stdin. + --display-name string + Edit the template display name. + + --dormancy-auto-deletion duration (default: 0h) + Specify a duration workspaces may be in the dormant state prior to + being deleted. This licensed feature's default is 0h (off). Maps to + "Dormancy Auto-Deletion" in the UI. + + --dormancy-threshold duration (default: 0h) + Specify a duration workspaces may be inactive prior to being moved to + the dormant state. This licensed feature's default is 0h (off). Maps + to "Dormancy threshold" in the UI. + + --failure-ttl duration (default: 0h) + Specify a failure TTL for workspaces created from this template. It is + the amount of time after a failed "start" build before coder + automatically schedules a "stop" build to cleanup.This licensed + feature's default is 0h (off). Maps to "Failure cleanup"in the UI. + + --icon string + Edit the template icon path. + --ignore-lockfile bool (default: false) Ignore warnings about not having a .terraform.lock.hcl file present in the template. + --max-ttl duration + Edit the template maximum time before shutdown - workspaces created + from this template must shutdown within the given duration after + starting. This is an enterprise-only feature. + -m, --message string Specify a message describing the changes in this version of the template. Messages longer than 72 characters will be displayed as @@ -32,9 +85,19 @@ OPTIONS: Specify a name for the new template version. It will be automatically generated if not provided. + --private bool + Disable the default behavior of granting template access to the + 'everyone' group. The template permissions must be updated to allow + non-admin users to use this template. + --provisioner-tag string-array Specify a set of tags to target provisioner daemons. + --require-active-version bool (default: false) + Requires workspace builds to use the active template version. This + setting does not apply to template admins. This is an enterprise-only + feature. + --var string-array Alias of --variable. From 2ad127bf3a06b56ff2082154d8de177da0ac046a Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 21 Dec 2023 20:10:04 +0000 Subject: [PATCH 12/19] fix tests: --- cli/templatepush_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cli/templatepush_test.go b/cli/templatepush_test.go index 5736df8cc2edf..29a43d0524dd3 100644 --- a/cli/templatepush_test.go +++ b/cli/templatepush_test.go @@ -676,10 +676,9 @@ func TestTemplatePush(t *testing.T) { args := []string{ "templates", "push", - templateName, + "my-template", "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), - "--create", } inv, root := clitest.New(t, args...) clitest.SetupConfig(t, templateAdmin, root) From f715efc861649283a57062795311a3ce87f01c38 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 21 Dec 2023 20:21:22 +0000 Subject: [PATCH 13/19] fix merge --- cli/templatecreate.go | 49 -------------------------------------- cli/templateedit.go | 55 ------------------------------------------- 2 files changed, 104 deletions(-) diff --git a/cli/templatecreate.go b/cli/templatecreate.go index c696dccf3a4ee..c4effa4dd4bbb 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -47,46 +47,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { r.InitClient(client), ), Handler: func(inv *clibase.Invocation) error { -<<<<<<< HEAD -<<<<<<< HEAD - isTemplateSchedulingOptionsSet := failureTTL != 0 || dormancyThreshold != 0 || dormancyAutoDeletion != 0 || maxTTL != 0 - - if isTemplateSchedulingOptionsSet || requireActiveVersion { - if failureTTL != 0 || dormancyThreshold != 0 || dormancyAutoDeletion != 0 { - // This call can be removed when workspace_actions is no longer experimental - experiments, exErr := client.Experiments(inv.Context()) - if exErr != nil { - return xerrors.Errorf("get experiments: %w", exErr) - } - - if !experiments.Enabled(codersdk.ExperimentWorkspaceActions) { - return xerrors.Errorf("--failure-ttl, --dormancy-threshold, and --dormancy-auto-deletion are experimental features. Use the workspace_actions CODER_EXPERIMENTS flag to set these configuration values.") - } - } - - entitlements, err := client.Entitlements(inv.Context()) - if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusNotFound { - return xerrors.Errorf("your deployment appears to be an AGPL deployment, so you cannot set enterprise-only flags") - } else if err != nil { - return xerrors.Errorf("get entitlements: %w", err) - } - - if isTemplateSchedulingOptionsSet { - if !entitlements.Features[codersdk.FeatureAdvancedTemplateScheduling].Enabled { - return xerrors.Errorf("your license is not entitled to use advanced template scheduling, so you cannot set --failure-ttl, --inactivity-ttl, or --max-ttl") - } - } - - if requireActiveVersion { - if !entitlements.Features[codersdk.FeatureAccessControl].Enabled { - return xerrors.Errorf("your license is not entitled to use enterprise access control, so you cannot set --require-active-version") - } - } -======= - err := handleEntitlements(inv.Context(), handleEntitlementsArgs{ -======= err := createEntitlementsCheck(inv.Context(), handleEntitlementsArgs{ ->>>>>>> 3c377e5d3 (combine edit flags) client: client, requireActiveVersion: requireActiveVersion, defaultTTL: defaultTTL, @@ -97,7 +58,6 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { }) if err != nil { return err ->>>>>>> 7b0afe8e9 (fix: make template push a superset of template create) } organization, err := CurrentOrganization(inv, client) @@ -420,15 +380,6 @@ func createEntitlementsCheck(ctx context.Context, args handleEntitlementsArgs) e if !entitlements.Features[codersdk.FeatureAccessControl].Enabled { return xerrors.Errorf("your license is not entitled to use enterprise access control, so you cannot set --require-active-version") } - - experiments, exErr := args.client.Experiments(ctx) - if exErr != nil { - return xerrors.Errorf("get experiments: %w", exErr) - } - - if !experiments.Enabled(codersdk.ExperimentTemplateUpdatePolicies) { - return xerrors.Errorf("--require-active-version is an experimental feature, contact an administrator to enable the 'template_update_policies' experiment on your Coder server") - } } } diff --git a/cli/templateedit.go b/cli/templateedit.go index df965d4b05937..b5d7682c9272b 100644 --- a/cli/templateedit.go +++ b/cli/templateedit.go @@ -47,52 +47,6 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { ), Short: "Edit the metadata of a template by name.", Handler: func(inv *clibase.Invocation) error { -<<<<<<< HEAD - // This clause can be removed when workspace_actions is no longer experimental - if failureTTL != 0 || dormancyThreshold != 0 || dormancyAutoDeletion != 0 { - experiments, exErr := client.Experiments(inv.Context()) - if exErr != nil { - return xerrors.Errorf("get experiments: %w", exErr) - } - - if !experiments.Enabled(codersdk.ExperimentWorkspaceActions) { - return xerrors.Errorf("--failure-ttl, --dormancy-threshold, and --dormancy-auto-deletion are experimental features. Use the workspace_actions CODER_EXPERIMENTS flag to set these configuration values.") - } - } - - unsetAutostopRequirementDaysOfWeek := len(autostopRequirementDaysOfWeek) == 1 && autostopRequirementDaysOfWeek[0] == "none" - requiresScheduling := (len(autostopRequirementDaysOfWeek) > 0 && !unsetAutostopRequirementDaysOfWeek) || - autostopRequirementWeeks > 0 || - !allowUserAutostart || - !allowUserAutostop || - maxTTL != 0 || - failureTTL != 0 || - dormancyThreshold != 0 || - dormancyAutoDeletion != 0 || - len(autostartRequirementDaysOfWeek) > 0 - - requiresEntitlement := requiresScheduling || requireActiveVersion - if requiresEntitlement { - entitlements, err := client.Entitlements(inv.Context()) - if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusNotFound { - return xerrors.Errorf("your deployment appears to be an AGPL deployment, so you cannot set enterprise-only flags") - } else if err != nil { - return xerrors.Errorf("get entitlements: %w", err) - } - - if requiresScheduling && !entitlements.Features[codersdk.FeatureAdvancedTemplateScheduling].Enabled { - return xerrors.Errorf("your license is not entitled to use advanced template scheduling, so you cannot set --max-ttl, --failure-ttl, --inactivityTTL, --allow-user-autostart=false or --allow-user-autostop=false") - } - - if requireActiveVersion { - if !entitlements.Features[codersdk.FeatureAccessControl].Enabled { - return xerrors.Errorf("your license is not entitled to use enterprise access control, so you cannot set --require-active-version") - } - } - } - -======= ->>>>>>> 3c377e5d3 (combine edit flags) organization, err := CurrentOrganization(inv, client) if err != nil { return xerrors.Errorf("get current organization: %w", err) @@ -341,15 +295,6 @@ func editTemplateEntitlementsCheck(ctx context.Context, args editTemplateEntitle if !entitlements.Features[codersdk.FeatureAccessControl].Enabled { return false, xerrors.Errorf("your license is not entitled to use enterprise access control, so you cannot set --require-active-version") } - - experiments, exErr := args.client.Experiments(ctx) - if exErr != nil { - return false, xerrors.Errorf("get experiments: %w", exErr) - } - - if !experiments.Enabled(codersdk.ExperimentTemplateUpdatePolicies) { - return false, xerrors.Errorf("--require-active-version is an experimental feature, contact an administrator to enable the 'template_update_policies' experiment on your Coder server") - } } } From 6269898a913efc39d32f02008a97f013ceaac95c Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 22 Dec 2023 14:49:12 +0000 Subject: [PATCH 14/19] add test --- cli/templatepush_test.go | 148 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/cli/templatepush_test.go b/cli/templatepush_test.go index 29a43d0524dd3..3365adc2742a4 100644 --- a/cli/templatepush_test.go +++ b/cli/templatepush_test.go @@ -6,8 +6,10 @@ import ( "os" "path/filepath" "runtime" + "strconv" "strings" "testing" + "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -708,6 +710,152 @@ func TestTemplatePush(t *testing.T) { require.NotEqual(t, uuid.Nil, template.ActiveVersionID) }) }) + + t.Run("EditMetadata", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + owner := coderdtest.CreateFirstUser(t, client) + templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin()) + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + + template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) + + // Test the cli command. + source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionApply: echo.ApplyComplete, + }) + + name := "new-template-name" + displayName := "New Display Name 789" + desc := "lorem ipsum dolor sit amet et cetera" + icon := "/icon/new-icon.png" + defaultTTL := 12 * time.Hour + allowUserCancelWorkspaceJobs := false + + inv, root := clitest.New(t, + "templates", + "push", + template.Name, + "--directory", source, + "--test.provisioner", string(database.ProvisionerTypeEcho), + "--name", name, + "--display-name", displayName, + "--description", desc, + "--icon", icon, + "--default-ttl", defaultTTL.String(), + "--allow-user-cancel-workspace-jobs="+strconv.FormatBool(allowUserCancelWorkspaceJobs), + ) + clitest.SetupConfig(t, templateAdmin, root) + pty := ptytest.New(t).Attach(inv) + + execDone := make(chan error) + go func() { + execDone <- inv.Run() + }() + + matches := []struct { + match string + write string + }{ + {match: "Upload", write: "yes"}, + } + for _, m := range matches { + pty.ExpectMatch(m.match) + pty.WriteLine(m.write) + } + + require.NoError(t, <-execDone) + + // Assert that the template version changed. + templateVersions, err := client.TemplateVersionsByTemplate(context.Background(), codersdk.TemplateVersionsByTemplateRequest{ + TemplateID: template.ID, + }) + require.NoError(t, err) + assert.Len(t, templateVersions, 2) + assert.NotEqual(t, template.ActiveVersionID, templateVersions[1].ID) + require.Equal(t, name, templateVersions[1].Name) + + // Assert that the template metadata changed. + updated, err := client.Template(context.Background(), template.ID) + require.NoError(t, err) + assert.Equal(t, template.Name, updated.Name) + assert.Equal(t, displayName, updated.DisplayName) + assert.Equal(t, desc, updated.Description) + assert.Equal(t, icon, updated.Icon) + assert.Equal(t, defaultTTL.Milliseconds(), updated.DefaultTTLMillis) + assert.Equal(t, allowUserCancelWorkspaceJobs, updated.AllowUserCancelWorkspaceJobs) + }) + + t.Run("EditMetadataNoSideEffects", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + owner := coderdtest.CreateFirstUser(t, client) + templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin()) + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + + template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) + + // Test the cli command. + source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionApply: echo.ApplyComplete, + }) + + desc := "lorem ipsum dolor sit amet et cetera" + + inv, root := clitest.New(t, + "templates", + "push", + template.Name, + "--directory", source, + "--test.provisioner", string(database.ProvisionerTypeEcho), + "--description", desc, + ) + clitest.SetupConfig(t, templateAdmin, root) + pty := ptytest.New(t).Attach(inv) + + execDone := make(chan error) + go func() { + execDone <- inv.Run() + }() + + matches := []struct { + match string + write string + }{ + {match: "Upload", write: "yes"}, + } + for _, m := range matches { + pty.ExpectMatch(m.match) + pty.WriteLine(m.write) + } + + require.NoError(t, <-execDone) + + // Assert that the template version changed. + templateVersions, err := client.TemplateVersionsByTemplate(context.Background(), codersdk.TemplateVersionsByTemplateRequest{ + TemplateID: template.ID, + }) + require.NoError(t, err) + assert.Len(t, templateVersions, 2) + assert.NotEqual(t, template.ActiveVersionID, templateVersions[1].ID) + + // Assert that the template metadata changed. + updated, err := client.Template(context.Background(), template.ID) + require.NoError(t, err) + // Changed + assert.Equal(t, desc, updated.Description) + + // Should not change + assert.Equal(t, template.Name, updated.Name) + assert.Equal(t, template.DisplayName, updated.DisplayName) + assert.Equal(t, template.Icon, updated.Icon) + assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis) + assert.Equal(t, template.AllowUserCancelWorkspaceJobs, updated.AllowUserCancelWorkspaceJobs) + }) } func createEchoResponsesWithTemplateVariables(templateVariables []*proto.TemplateVariable) *echo.Responses { From d1cf91961f8967ce16e3e81ef817a49bbe9c9805 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 22 Dec 2023 14:49:31 +0000 Subject: [PATCH 15/19] remove test --- cli/templatepush_test.go | 69 ---------------------------------------- 1 file changed, 69 deletions(-) diff --git a/cli/templatepush_test.go b/cli/templatepush_test.go index 3365adc2742a4..0fa24a259cb8e 100644 --- a/cli/templatepush_test.go +++ b/cli/templatepush_test.go @@ -787,75 +787,6 @@ func TestTemplatePush(t *testing.T) { assert.Equal(t, defaultTTL.Milliseconds(), updated.DefaultTTLMillis) assert.Equal(t, allowUserCancelWorkspaceJobs, updated.AllowUserCancelWorkspaceJobs) }) - - t.Run("EditMetadataNoSideEffects", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - owner := coderdtest.CreateFirstUser(t, client) - templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin()) - version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) - _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) - - template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) - - // Test the cli command. - source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionApply: echo.ApplyComplete, - }) - - desc := "lorem ipsum dolor sit amet et cetera" - - inv, root := clitest.New(t, - "templates", - "push", - template.Name, - "--directory", source, - "--test.provisioner", string(database.ProvisionerTypeEcho), - "--description", desc, - ) - clitest.SetupConfig(t, templateAdmin, root) - pty := ptytest.New(t).Attach(inv) - - execDone := make(chan error) - go func() { - execDone <- inv.Run() - }() - - matches := []struct { - match string - write string - }{ - {match: "Upload", write: "yes"}, - } - for _, m := range matches { - pty.ExpectMatch(m.match) - pty.WriteLine(m.write) - } - - require.NoError(t, <-execDone) - - // Assert that the template version changed. - templateVersions, err := client.TemplateVersionsByTemplate(context.Background(), codersdk.TemplateVersionsByTemplateRequest{ - TemplateID: template.ID, - }) - require.NoError(t, err) - assert.Len(t, templateVersions, 2) - assert.NotEqual(t, template.ActiveVersionID, templateVersions[1].ID) - - // Assert that the template metadata changed. - updated, err := client.Template(context.Background(), template.ID) - require.NoError(t, err) - // Changed - assert.Equal(t, desc, updated.Description) - - // Should not change - assert.Equal(t, template.Name, updated.Name) - assert.Equal(t, template.DisplayName, updated.DisplayName) - assert.Equal(t, template.Icon, updated.Icon) - assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis) - assert.Equal(t, template.AllowUserCancelWorkspaceJobs, updated.AllowUserCancelWorkspaceJobs) - }) } func createEchoResponsesWithTemplateVariables(templateVariables []*proto.TemplateVariable) *echo.Responses { From 6bcc688f0ad9338ebfb106c1c493abb4aaafe7be Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 22 Dec 2023 15:11:07 +0000 Subject: [PATCH 16/19] add unset test --- cli/templateedit.go | 8 +++++ cli/templatepush_test.go | 71 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/cli/templateedit.go b/cli/templateedit.go index b5d7682c9272b..ebe5330bba0d4 100644 --- a/cli/templateedit.go +++ b/cli/templateedit.go @@ -342,6 +342,14 @@ func updateTemplateMetaRequest(args updateTemplateMetaArgs) codersdk.UpdateTempl if args.unsetAutostopRequirementDaysOfWeek { args.autostopRequirementDaysOfWeek = []string{} } + unsetDefaultTTL, err := time.ParseDuration("24h") + if err != nil { + panic(err) + } + + if args.defaultTTL == unsetDefaultTTL { + args.defaultTTL = time.Duration(args.template.DefaultTTLMillis) * time.Millisecond + } if args.failureTTL == 0 { args.failureTTL = time.Duration(args.template.FailureTTLMillis) * time.Millisecond } diff --git a/cli/templatepush_test.go b/cli/templatepush_test.go index 0fa24a259cb8e..c75cb543e6461 100644 --- a/cli/templatepush_test.go +++ b/cli/templatepush_test.go @@ -787,6 +787,77 @@ func TestTemplatePush(t *testing.T) { assert.Equal(t, defaultTTL.Milliseconds(), updated.DefaultTTLMillis) assert.Equal(t, allowUserCancelWorkspaceJobs, updated.AllowUserCancelWorkspaceJobs) }) + + t.Run("EditMetadataKeepUnsetUnchanged", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + owner := coderdtest.CreateFirstUser(t, client) + templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin()) + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil) + _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + + template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) + + // Test the cli command. + source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionApply: echo.ApplyComplete, + }) + + desc := "lorem ipsum dolor sit amet et cetera" + + inv, root := clitest.New(t, + "templates", + "push", + template.Name, + "--directory", source, + "--test.provisioner", string(database.ProvisionerTypeEcho), + "--description", desc, + ) + clitest.SetupConfig(t, templateAdmin, root) + pty := ptytest.New(t).Attach(inv) + + execDone := make(chan error) + go func() { + execDone <- inv.Run() + }() + + matches := []struct { + match string + write string + }{ + {match: "Upload", write: "yes"}, + } + for _, m := range matches { + pty.ExpectMatch(m.match) + pty.WriteLine(m.write) + } + + require.NoError(t, <-execDone) + + // Assert that the template version changed. + templateVersions, err := client.TemplateVersionsByTemplate(context.Background(), codersdk.TemplateVersionsByTemplateRequest{ + TemplateID: template.ID, + }) + require.NoError(t, err) + assert.Len(t, templateVersions, 2) + assert.NotEqual(t, template.ActiveVersionID, templateVersions[1].ID) + + // Assert that the template metadata changed. + updated, err := client.Template(context.Background(), template.ID) + require.NoError(t, err) + assert.Equal(t, template.Name, updated.Name) + assert.Equal(t, template.DisplayName, updated.DisplayName) + assert.Equal(t, desc, updated.Description) + assert.Equal(t, template.Icon, updated.Icon) + assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis) + assert.Equal(t, template.FailureTTLMillis, updated.FailureTTLMillis) + assert.Equal(t, template.TimeTilDormantAutoDeleteMillis, updated.TimeTilDormantAutoDeleteMillis) + assert.Equal(t, template.AutostartRequirement.DaysOfWeek, updated.AutostartRequirement.DaysOfWeek) + assert.Equal(t, template.AutostopRequirement.DaysOfWeek, updated.AutostopRequirement.DaysOfWeek) + assert.Equal(t, template.RequireActiveVersion, updated.RequireActiveVersion) + assert.Equal(t, template.AllowUserCancelWorkspaceJobs, updated.AllowUserCancelWorkspaceJobs) + }) } func createEchoResponsesWithTemplateVariables(templateVariables []*proto.TemplateVariable) *echo.Responses { From a5ac9b708442ce296960709baf31ec599987c317 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 22 Dec 2023 15:19:21 +0000 Subject: [PATCH 17/19] Add deprecation warning --- cli/templatecreate.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cli/templatecreate.go b/cli/templatecreate.go index c4effa4dd4bbb..e9a483fb3db0f 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -47,6 +47,11 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { r.InitClient(client), ), Handler: func(inv *clibase.Invocation) error { + _, _ = fmt.Fprintln(inv.Stdout, "\n"+pretty.Sprint(cliui.DefaultStyles.Wrap, + pretty.Sprint( + cliui.DefaultStyles.Warn, "DEPRECATION WARNING: The `coder templates push` command should be used instead. This command will be removed in a future release. ")+"\n")) + time.Sleep(1 * time.Second) + err := createEntitlementsCheck(inv.Context(), handleEntitlementsArgs{ client: client, requireActiveVersion: requireActiveVersion, From 132efda9a78a65c8f0b8fb3145cee6e34edc1884 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 22 Dec 2023 15:44:21 +0000 Subject: [PATCH 18/19] fix --- cli/templatepush_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/templatepush_test.go b/cli/templatepush_test.go index c75cb543e6461..0c129f9ad07f4 100644 --- a/cli/templatepush_test.go +++ b/cli/templatepush_test.go @@ -856,7 +856,6 @@ func TestTemplatePush(t *testing.T) { assert.Equal(t, template.AutostartRequirement.DaysOfWeek, updated.AutostartRequirement.DaysOfWeek) assert.Equal(t, template.AutostopRequirement.DaysOfWeek, updated.AutostopRequirement.DaysOfWeek) assert.Equal(t, template.RequireActiveVersion, updated.RequireActiveVersion) - assert.Equal(t, template.AllowUserCancelWorkspaceJobs, updated.AllowUserCancelWorkspaceJobs) }) } From 9da9aed2c851688a60fad506272700c482a4f6f2 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Tue, 2 Jan 2024 18:31:45 +0000 Subject: [PATCH 19/19] rename functions --- cli/templatecreate.go | 6 +++--- cli/templateedit.go | 6 +++--- cli/templatepush.go | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cli/templatecreate.go b/cli/templatecreate.go index e9a483fb3db0f..62de354fc4c1c 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -52,7 +52,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { cliui.DefaultStyles.Warn, "DEPRECATION WARNING: The `coder templates push` command should be used instead. This command will be removed in a future release. ")+"\n")) time.Sleep(1 * time.Second) - err := createEntitlementsCheck(inv.Context(), handleEntitlementsArgs{ + err := checkTemplateCreateEntitlements(inv.Context(), checkTemplateCreateEntitlementsArgs{ client: client, requireActiveVersion: requireActiveVersion, defaultTTL: defaultTTL, @@ -342,7 +342,7 @@ func ParseProvisionerTags(rawTags []string) (map[string]string, error) { return tags, nil } -type handleEntitlementsArgs struct { +type checkTemplateCreateEntitlementsArgs struct { client *codersdk.Client requireActiveVersion bool defaultTTL time.Duration @@ -352,7 +352,7 @@ type handleEntitlementsArgs struct { maxTTL time.Duration } -func createEntitlementsCheck(ctx context.Context, args handleEntitlementsArgs) error { +func checkTemplateCreateEntitlements(ctx context.Context, args checkTemplateCreateEntitlementsArgs) error { isTemplateSchedulingOptionsSet := args.failureTTL != 0 || args.dormancyThreshold != 0 || args.dormancyAutoDeletion != 0 || args.maxTTL != 0 if isTemplateSchedulingOptionsSet || args.requireActiveVersion { diff --git a/cli/templateedit.go b/cli/templateedit.go index ebe5330bba0d4..39f7c34537fb4 100644 --- a/cli/templateedit.go +++ b/cli/templateedit.go @@ -56,7 +56,7 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { return xerrors.Errorf("get workspace template: %w", err) } - unsetAutostopRequirementDaysOfWeek, err := editTemplateEntitlementsCheck(inv.Context(), editTemplateEntitlementsArgs{ + unsetAutostopRequirementDaysOfWeek, err := checkEditTemplateEntitlements(inv.Context(), checkEditTemplateEntitlementsArgs{ client: client, inv: inv, @@ -236,7 +236,7 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { return cmd } -type editTemplateEntitlementsArgs struct { +type checkEditTemplateEntitlementsArgs struct { client *codersdk.Client inv *clibase.Invocation @@ -254,7 +254,7 @@ type editTemplateEntitlementsArgs struct { requireActiveVersion bool } -func editTemplateEntitlementsCheck(ctx context.Context, args editTemplateEntitlementsArgs) (bool, error) { +func checkEditTemplateEntitlements(ctx context.Context, args checkEditTemplateEntitlementsArgs) (bool, error) { // This clause can be removed when workspace_actions is no longer experimental if args.failureTTL != 0 || args.dormancyThreshold != 0 || args.dormancyAutoDeletion != 0 { experiments, exErr := args.client.Experiments(ctx) diff --git a/cli/templatepush.go b/cli/templatepush.go index 218bb062b6168..0b1d765d0b4f1 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -198,7 +198,7 @@ func (r *RootCmd) templatePush() *clibase.Cmd { Handler: func(inv *clibase.Invocation) error { uploadFlags.setWorkdir(workdir) - err := createEntitlementsCheck(inv.Context(), handleEntitlementsArgs{ + err := checkTemplateCreateEntitlements(inv.Context(), checkTemplateCreateEntitlementsArgs{ client: client, requireActiveVersion: requireActiveVersion, defaultTTL: defaultTTL, @@ -211,7 +211,7 @@ func (r *RootCmd) templatePush() *clibase.Cmd { return err } - unsetAutostopRequirementDaysOfWeek, err := editTemplateEntitlementsCheck(inv.Context(), editTemplateEntitlementsArgs{ + unsetAutostopRequirementDaysOfWeek, err := checkEditTemplateEntitlements(inv.Context(), checkEditTemplateEntitlementsArgs{ client: client, inv: inv, defaultTTL: defaultTTL,