From 0cc46c8c8418413ab80eb63b7093c7d6cab4c840 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 20 Dec 2023 13:48:12 +0000 Subject: [PATCH 01/43] 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 93da7d03c2fdb4b70edd0d0369001a0d957c8d2b Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 20 Dec 2023 13:49:20 +0000 Subject: [PATCH 02/43] 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 3c409f6c712c4124618aec94fe163e06c42ea860 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 20 Dec 2023 14:10:04 +0000 Subject: [PATCH 03/43] 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 17b3091cfe2a5..b38c1b48298eb 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1265,6 +1265,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 4b763d7933a3b4bc7ee10bbbb1a6ea1309f4943f Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 20 Dec 2023 14:11:37 +0000 Subject: [PATCH 04/43] 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 d178cc7435004fb3e5a948713a9f2fb8e2898002 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 20 Dec 2023 18:33:02 +0000 Subject: [PATCH 05/43] 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 547494292128a1289e29fe7f658d4f572741c9f1 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 20 Dec 2023 18:33:48 +0000 Subject: [PATCH 06/43] 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 2b9bcf63b30decc7538541638b8b83b9a598620d Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 20 Dec 2023 20:50:52 +0000 Subject: [PATCH 07/43] 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 44f264b430c63688a29c6f4dec3106c5570246e7 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 20 Dec 2023 20:54:09 +0000 Subject: [PATCH 08/43] 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 d58cf185ddbef18d1d0b5531095c051865c2ab05 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 21 Dec 2023 16:05:38 +0000 Subject: [PATCH 09/43] 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 9b329221a11b84a40ad3292585da623255bcd36d Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 21 Dec 2023 16:20:03 +0000 Subject: [PATCH 10/43] 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 b402e92a947f2..39f722f59bdb1 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 c6ef6f4aeea34265451976ef0537070f29e3311e Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 21 Dec 2023 16:36:07 +0000 Subject: [PATCH 11/43] 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 e1e1653c59442057466edc5e7661aa3076612010 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 21 Dec 2023 20:10:04 +0000 Subject: [PATCH 12/43] 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 0cdefbc6c1f9b1a0bb3b14f86c7c50b3f73ab366 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 21 Dec 2023 20:21:22 +0000 Subject: [PATCH 13/43] 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 d2a7866177a0d671ee6d0b214c4bc0bc248e1b1c Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 22 Dec 2023 14:49:12 +0000 Subject: [PATCH 14/43] 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 63d57fd94f6b3919cae5b8cac0d915205cc8bc4a Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 22 Dec 2023 14:49:31 +0000 Subject: [PATCH 15/43] 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 ca65869d5a21f8e75bd229cefa28fd292a5929a4 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 22 Dec 2023 15:11:07 +0000 Subject: [PATCH 16/43] 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 9866235739ea23647df829d229e4414ecdc6939a Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 22 Dec 2023 15:19:21 +0000 Subject: [PATCH 17/43] 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 35c7adfee436c2c809dedae6b8e77d8d9f5ec013 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 22 Dec 2023 15:44:21 +0000 Subject: [PATCH 18/43] 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 4abf179a897dda345a2446e3f55902557ea5b4c8 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Tue, 2 Jan 2024 18:31:45 +0000 Subject: [PATCH 19/43] 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, From a3fdb7661ef402986bcfe3bed4006324845038fa Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 3 Jan 2024 15:21:33 +0000 Subject: [PATCH 20/43] test removing flags --- cli/templatecreate.go | 126 +---------------- cli/templateedit.go | 313 ++++++++++++++---------------------------- cli/templatepush.go | 212 +--------------------------- 3 files changed, 107 insertions(+), 544 deletions(-) diff --git a/cli/templatecreate.go b/cli/templatecreate.go index 62de354fc4c1c..a2905ef8a8469 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -1,11 +1,9 @@ package cli import ( - "context" "errors" "fmt" "io" - "net/http" "strings" "time" "unicode/utf8" @@ -17,7 +15,6 @@ import ( "github.com/coder/coder/v2/cli/clibase" "github.com/coder/coder/v2/cli/cliui" - "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" ) @@ -27,14 +24,6 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { provisionerTags []string variablesFile string commandLineVariables []string - disableEveryone bool - requireActiveVersion bool - - defaultTTL time.Duration - failureTTL time.Duration - dormancyThreshold time.Duration - dormancyAutoDeletion time.Duration - maxTTL time.Duration uploadFlags templateUploadFlags ) @@ -52,19 +41,6 @@ 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 := checkTemplateCreateEntitlements(inv.Context(), checkTemplateCreateEntitlementsArgs{ - 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 { return err @@ -133,15 +109,8 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { } createReq := codersdk.CreateTemplateRequest{ - Name: templateName, - VersionID: job.ID, - DefaultTTLMillis: ptr.Ref(defaultTTL.Milliseconds()), - FailureTTLMillis: ptr.Ref(failureTTL.Milliseconds()), - MaxTTLMillis: ptr.Ref(maxTTL.Milliseconds()), - TimeTilDormantMillis: ptr.Ref(dormancyThreshold.Milliseconds()), - TimeTilDormantAutoDeleteMillis: ptr.Ref(dormancyAutoDeletion.Milliseconds()), - DisableEveryoneGroupAccess: disableEveryone, - RequireActiveVersion: requireActiveVersion, + Name: templateName, + VersionID: job.ID, } _, err = client.CreateTemplate(inv.Context(), organization.ID, createReq) @@ -162,12 +131,6 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { }, } cmd.Options = clibase.OptionSet{ - { - 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), - }, { Flag: "variables-file", Description: "Specify a file path with values for Terraform-managed variables.", @@ -188,36 +151,6 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { Description: "Specify a set of tags to target provisioner daemons.", Value: clibase.StringArrayOf(&provisionerTags), }, - { - 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: "test.provisioner", Description: "Customize the provisioner backend.", @@ -225,12 +158,6 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { Value: clibase.StringOf(&provisioner), Hidden: true, }, - { - 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", - }, cliui.SkipPromptOption(), } @@ -341,52 +268,3 @@ func ParseProvisionerTags(rawTags []string) (map[string]string, error) { } return tags, nil } - -type checkTemplateCreateEntitlementsArgs struct { - client *codersdk.Client - requireActiveVersion bool - defaultTTL time.Duration - failureTTL time.Duration - dormancyThreshold time.Duration - dormancyAutoDeletion time.Duration - maxTTL time.Duration -} - -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 { - 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") - } - } - } - - return nil -} diff --git a/cli/templateedit.go b/cli/templateedit.go index 39f7c34537fb4..9cbcefc88730f 100644 --- a/cli/templateedit.go +++ b/cli/templateedit.go @@ -1,7 +1,6 @@ package cli import ( - "context" "fmt" "net/http" "strings" @@ -47,6 +46,49 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { ), Short: "Edit the metadata of a template by name.", Handler: func(inv *clibase.Invocation) error { + // 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") + } + } + } + organization, err := CurrentOrganization(inv, client) if err != nil { return xerrors.Errorf("get current organization: %w", err) @@ -56,51 +98,71 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { return xerrors.Errorf("get workspace template: %w", err) } - unsetAutostopRequirementDaysOfWeek, err := checkEditTemplateEntitlements(inv.Context(), checkEditTemplateEntitlementsArgs{ - client: client, - inv: inv, + // 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 + } - defaultTTL: defaultTTL, - maxTTL: maxTTL, - autostopRequirementDaysOfWeek: autostopRequirementDaysOfWeek, - autostopRequirementWeeks: autostopRequirementWeeks, - autostartRequirementDaysOfWeek: autostartRequirementDaysOfWeek, - failureTTL: failureTTL, - dormancyThreshold: dormancyThreshold, - dormancyAutoDeletion: dormancyAutoDeletion, - allowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs, - allowUserAutostart: allowUserAutostart, - allowUserAutostop: allowUserAutostop, - requireActiveVersion: requireActiveVersion, - }) - if err != nil { - return err + // Default values + if !userSetOption(inv, "description") { + description = template.Description + } + + if !userSetOption(inv, "icon") { + icon = template.Icon + } + + 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 + } - 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, - }) + 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, + } _, err = client.UpdateTemplateMeta(inv.Context(), template.ID, req) if err != nil { @@ -235,170 +297,3 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { return cmd } - -type checkEditTemplateEntitlementsArgs 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 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) - 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") - } - } - } - - 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{} - } - 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 - } - 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 0b1d765d0b4f1..600700f9d944f 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -168,24 +168,6 @@ func (r *RootCmd) templatePush() *clibase.Cmd { provisionerTags []string uploadFlags templateUploadFlags activate bool - - 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{ @@ -198,39 +180,6 @@ func (r *RootCmd) templatePush() *clibase.Cmd { Handler: func(inv *clibase.Invocation) error { uploadFlags.setWorkdir(workdir) - err := checkTemplateCreateEntitlements(inv.Context(), checkTemplateCreateEntitlementsArgs{ - client: client, - requireActiveVersion: requireActiveVersion, - defaultTTL: defaultTTL, - failureTTL: failureTTL, - dormancyThreshold: dormancyThreshold, - dormancyAutoDeletion: dormancyAutoDeletion, - maxTTL: maxTTL, - }) - if err != nil { - return err - } - - unsetAutostopRequirementDaysOfWeek, err := checkEditTemplateEntitlements(inv.Context(), checkEditTemplateEntitlementsArgs{ - 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, - }) - if err != nil { - return err - } - organization, err := CurrentOrganization(inv, client) if err != nil { return err @@ -333,35 +282,7 @@ func (r *RootCmd) templatePush() *clibase.Cmd { 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 } @@ -413,137 +334,6 @@ 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.", - Value: clibase.BoolOf(&alwaysPrompt), - }, - { - Flag: "activate", - Description: "Whether the new template will be marked active.", - Default: "true", - Value: clibase.BoolOf(&activate), - }, - { - 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", - }, - { - 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), - }, - { - 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: "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 7599090a59bcacef513eb424945fef1e2a8bd8ab Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 3 Jan 2024 15:26:45 +0000 Subject: [PATCH 21/43] revert --- cli/templatepush.go | 11 -- cli/templatepush_test.go | 149 ------------------ .../coder_templates_push_--help.golden | 67 +------- coderd/database/queries.sql.go | 19 +-- coderd/database/queries/templates.sql | 3 +- coderd/templates.go | 6 - codersdk/templates.go | 6 - .../TemplateSettingsForm.tsx | 1 - .../TemplateSettingsPage.test.tsx | 1 - .../TemplateScheduleForm.tsx | 2 - .../TemplateSchedulePage.test.tsx | 1 - 11 files changed, 11 insertions(+), 255 deletions(-) diff --git a/cli/templatepush.go b/cli/templatepush.go index 600700f9d944f..ccb5477e2d6f3 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -277,17 +277,6 @@ func (r *RootCmd) templatePush() *clibase.Cmd { _, _ = fmt.Fprintf(inv.Stdout, "Updated version at %s!\n", pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp))) - // refresh template data for edit api call - template, err = client.TemplateByName(inv.Context(), organization.ID, name) - if err != nil { - return 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))) - return nil }, } diff --git a/cli/templatepush_test.go b/cli/templatepush_test.go index 0c129f9ad07f4..29a43d0524dd3 100644 --- a/cli/templatepush_test.go +++ b/cli/templatepush_test.go @@ -6,10 +6,8 @@ import ( "os" "path/filepath" "runtime" - "strconv" "strings" "testing" - "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" @@ -710,153 +708,6 @@ 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("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) - }) } func createEchoResponsesWithTemplateVariables(templateVariables []*proto.TemplateVariable) *echo.Responses { diff --git a/cli/testdata/coder_templates_push_--help.golden b/cli/testdata/coder_templates_push_--help.golden index e484c143e9971..9d255c1f8bc23 100644 --- a/cli/testdata/coder_templates_push_--help.golden +++ b/cli/testdata/coder_templates_push_--help.golden @@ -6,76 +6,23 @@ 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. - --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. + --create bool (default: false) + Create the template if it does not exist. -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 @@ -85,19 +32,9 @@ 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. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 81bbe52386cf9..2a1f3b316c650 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6075,21 +6075,19 @@ SET name = $4, icon = $5, display_name = $6, - allow_user_cancel_workspace_jobs = $7, - group_acl = $8 + allow_user_cancel_workspace_jobs = $7 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"` - GroupACL TemplateACL `db:"group_acl" json:"group_acl"` + 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"` } func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error { @@ -6101,7 +6099,6 @@ 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 ca031bb0bd839..af8c3fe80f420 100644 --- a/coderd/database/queries/templates.sql +++ b/coderd/database/queries/templates.sql @@ -115,8 +115,7 @@ SET name = $4, icon = $5, display_name = $6, - allow_user_cancel_workspace_jobs = $7, - group_acl = $8 + allow_user_cancel_workspace_jobs = $7 WHERE id = $1 ; diff --git a/coderd/templates.go b/coderd/templates.go index d4c33a454ce16..5e6d9644a782f 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -667,11 +667,6 @@ 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, @@ -681,7 +676,6 @@ 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 1be4d931ad7a2..8164843ad0c66 100644 --- a/codersdk/templates.go +++ b/codersdk/templates.go @@ -241,12 +241,6 @@ 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/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx index 39f722f59bdb1..b402e92a947f2 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx @@ -77,7 +77,6 @@ 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 ee6153646584a..be0d593c9e13e 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx @@ -47,7 +47,6 @@ 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 89f26cc5d451e..f1f0af511ec9b 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx @@ -118,7 +118,6 @@ 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: () => { @@ -239,7 +238,6 @@ 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 1a5618cf3263a..77e50d73f0657 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx @@ -28,7 +28,6 @@ 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 e958e1a379d814619490637b8c434fb4ca4b1d92 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 3 Jan 2024 16:00:09 +0000 Subject: [PATCH 22/43] cleanup --- cli/templatecreate.go | 69 ++++++------------ cli/templatepush.go | 160 ++++++++++++++++++++++++++---------------- 2 files changed, 122 insertions(+), 107 deletions(-) diff --git a/cli/templatecreate.go b/cli/templatecreate.go index a2905ef8a8469..57a1280d750fb 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -6,7 +6,6 @@ import ( "io" "strings" "time" - "unicode/utf8" "github.com/google/uuid" "golang.org/x/xerrors" @@ -24,6 +23,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { provisionerTags []string variablesFile string commandLineVariables []string + versionName string uploadFlags templateUploadFlags ) @@ -46,53 +46,23 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { return err } - templateName, err := uploadFlags.templateName(inv.Args) + name, err := uploadFlags.templateName(inv.Args) if err != nil { return err } - if utf8.RuneCountInString(templateName) > 31 { - return xerrors.Errorf("Template name must be less than 32 characters") - } - - _, err = client.TemplateByName(inv.Context(), organization.ID, templateName) - if err == nil { - return xerrors.Errorf("A template already exists named %q!", templateName) - } - - err = uploadFlags.checkForLockfile(inv) - if err != nil { - return xerrors.Errorf("check for lockfile: %w", err) - } - - message := uploadFlags.templateMessage(inv) - - // Confirm upload of the directory. - resp, err := uploadFlags.upload(inv, client) - if err != nil { - return err - } - - tags, err := ParseProvisionerTags(provisionerTags) - if err != nil { - return err - } - - userVariableValues, err := ParseUserVariableValues( - variablesFile, - commandLineVariables) - if err != nil { - return err - } - - job, err := createValidTemplateVersion(inv, createValidTemplateVersionArgs{ - Message: message, - Client: client, - Organization: organization, - Provisioner: codersdk.ProvisionerType(provisioner), - FileID: resp.ID, - ProvisionerTags: tags, - UserVariableValues: userVariableValues, + job, _, err := createTemplateVersion(createTemplateVersionArgs{ + inv: inv, + client: client, + name: name, + org: organization, + uploadFlags: uploadFlags, + provisionerTags: provisionerTags, + provisioner: provisioner, + variablesFile: variablesFile, + commandLineVariables: commandLineVariables, + versionName: versionName, + alwaysPrompt: false, }) if err != nil { return err @@ -109,7 +79,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { } createReq := codersdk.CreateTemplateRequest{ - Name: templateName, + Name: name, VersionID: job.ID, } @@ -120,11 +90,11 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { _, _ = fmt.Fprintln(inv.Stdout, "\n"+pretty.Sprint(cliui.DefaultStyles.Wrap, "The "+pretty.Sprint( - cliui.DefaultStyles.Keyword, templateName)+" template has been created at "+ + cliui.DefaultStyles.Keyword, name)+" template has been created at "+ pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp))+"! "+ "Developers can provision a workspace with this template using:")+"\n") - _, _ = fmt.Fprintln(inv.Stdout, " "+pretty.Sprint(cliui.DefaultStyles.Code, fmt.Sprintf("coder create --template=%q [workspace name]", templateName))) + _, _ = fmt.Fprintln(inv.Stdout, " "+pretty.Sprint(cliui.DefaultStyles.Code, fmt.Sprintf("coder create --template=%q [workspace name]", name))) _, _ = fmt.Fprintln(inv.Stdout) return nil @@ -158,6 +128,11 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { Value: clibase.StringOf(&provisioner), Hidden: true, }, + { + Flag: "name", + Description: "Specify a name for the new template version. It will be automatically generated if not provided.", + Value: clibase.StringOf(&versionName), + }, cliui.SkipPromptOption(), } diff --git a/cli/templatepush.go b/cli/templatepush.go index ccb5477e2d6f3..8658de42032e0 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -190,70 +190,24 @@ 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 { - var apiError *codersdk.Error - if errors.As(err, &apiError) && apiError.StatusCode() != http.StatusNotFound { - return err - } - createTemplate = true - } - - err = uploadFlags.checkForLockfile(inv) - if err != nil { - return xerrors.Errorf("check for lockfile: %w", err) - } - - message := uploadFlags.templateMessage(inv) - - resp, err := uploadFlags.upload(inv, client) - if err != nil { - return err - } - - tags, err := ParseProvisionerTags(provisionerTags) - if err != nil { - return err - } - - userVariableValues, err := ParseUserVariableValues( - variablesFile, - commandLineVariables) - if err != nil { - return err - } - - args := createValidTemplateVersionArgs{ - Message: message, - Client: client, - Organization: organization, - Provisioner: codersdk.ProvisionerType(provisioner), - FileID: resp.ID, - ProvisionerTags: tags, - UserVariableValues: userVariableValues, - } - - if !createTemplate { - args.Name = versionName - args.Template = &template - args.ReuseParameters = !alwaysPrompt - } - - job, err := createValidTemplateVersion(inv, args) + job, template, err := createTemplateVersion(createTemplateVersionArgs{ + inv: inv, + client: client, + name: name, + org: organization, + uploadFlags: uploadFlags, + provisionerTags: provisionerTags, + provisioner: provisioner, + variablesFile: variablesFile, + commandLineVariables: commandLineVariables, + versionName: versionName, + alwaysPrompt: alwaysPrompt, + }) if err != nil { return err } - if job.Job.Status != codersdk.ProvisionerJobSucceeded { - return xerrors.Errorf("job failed: %s", job.Job.Status) - } - - if createTemplate { + if template == nil { _, err = client.CreateTemplate(inv.Context(), organization.ID, codersdk.CreateTemplateRequest{ Name: name, VersionID: job.ID, @@ -323,6 +277,11 @@ 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: "always-prompt", + Description: "Always prompt all parameters. Does not pull parameter values from active template version.", + Value: clibase.BoolOf(&alwaysPrompt), + }, cliui.SkipPromptOption(), } cmd.Options = append(cmd.Options, uploadFlags.options()...) @@ -345,3 +304,84 @@ func prettyDirectoryPath(dir string) string { } return prettyDir } + +type createTemplateVersionArgs struct { + inv *clibase.Invocation + client *codersdk.Client + name string + org codersdk.Organization + uploadFlags templateUploadFlags + provisionerTags []string + provisioner string + variablesFile string + commandLineVariables []string + versionName string + alwaysPrompt bool +} + +func createTemplateVersion(args createTemplateVersionArgs) (*codersdk.TemplateVersion, *codersdk.Template, error) { + if utf8.RuneCountInString(args.name) >= 32 { + return nil, nil, xerrors.Errorf("Template name must be less than 32 characters") + } + + var createTemplate bool + template, err := args.client.TemplateByName(args.inv.Context(), args.org.ID, args.name) + if err != nil { + var apiError *codersdk.Error + if errors.As(err, &apiError) && apiError.StatusCode() != http.StatusNotFound { + return nil, nil, err + } + createTemplate = true + } + + err = args.uploadFlags.checkForLockfile(args.inv) + if err != nil { + return nil, nil, xerrors.Errorf("check for lockfile: %w", err) + } + + message := args.uploadFlags.templateMessage(args.inv) + + resp, err := args.uploadFlags.upload(args.inv, args.client) + if err != nil { + return nil, nil, err + } + + tags, err := ParseProvisionerTags(args.provisionerTags) + if err != nil { + return nil, nil, err + } + + userVariableValues, err := ParseUserVariableValues( + args.variablesFile, + args.commandLineVariables) + if err != nil { + return nil, nil, err + } + + versionArgs := createValidTemplateVersionArgs{ + Message: message, + Client: args.client, + Organization: args.org, + Provisioner: codersdk.ProvisionerType(args.provisioner), + FileID: resp.ID, + ProvisionerTags: tags, + UserVariableValues: userVariableValues, + } + + if !createTemplate { + versionArgs.Name = args.versionName + versionArgs.Template = &template + versionArgs.ReuseParameters = !args.alwaysPrompt + } + + job, err := createValidTemplateVersion(args.inv, versionArgs) + if err != nil { + return nil, nil, err + } + + if job.Job.Status != codersdk.ProvisionerJobSucceeded { + return nil, nil, xerrors.Errorf("job failed: %s", job.Job.Status) + } + + return job, &template, nil +} From c8cae6c60408fd4a73fd9a6cfd8143b38195d030 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Wed, 3 Jan 2024 18:49:21 +0000 Subject: [PATCH 23/43] add back flag --- cli/templatepush.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cli/templatepush.go b/cli/templatepush.go index 8658de42032e0..e042addae6134 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -282,6 +282,12 @@ func (r *RootCmd) templatePush() *clibase.Cmd { Description: "Always prompt all parameters. Does not pull parameter values from active template version.", Value: clibase.BoolOf(&alwaysPrompt), }, + { + Flag: "activate", + Description: "Whether the new template will be marked active.", + Default: "true", + Value: clibase.BoolOf(&activate), + }, cliui.SkipPromptOption(), } cmd.Options = append(cmd.Options, uploadFlags.options()...) From 1b03e8934e5cc57250fb84ecc12296acb6e3ef04 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 4 Jan 2024 14:20:16 +0000 Subject: [PATCH 24/43] remove create command --- cli/templatecreate.go | 235 +------------------- cli/templatecreate_test.go | 432 ------------------------------------- cli/templatepush.go | 216 ++++++++++++------- cli/templatepush_test.go | 63 +++++- 4 files changed, 211 insertions(+), 735 deletions(-) delete mode 100644 cli/templatecreate_test.go diff --git a/cli/templatecreate.go b/cli/templatecreate.go index 57a1280d750fb..cafae32712f10 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -1,245 +1,28 @@ package cli import ( - "errors" "fmt" - "io" - "strings" - "time" - - "github.com/google/uuid" - "golang.org/x/xerrors" - - "github.com/coder/pretty" "github.com/coder/coder/v2/cli/clibase" "github.com/coder/coder/v2/cli/cliui" - "github.com/coder/coder/v2/codersdk" + "github.com/coder/pretty" ) -func (r *RootCmd) templateCreate() *clibase.Cmd { - var ( - provisioner string - provisionerTags []string - variablesFile string - commandLineVariables []string - versionName string - - uploadFlags templateUploadFlags - ) - client := new(codersdk.Client) +func (*RootCmd) templateCreate() *clibase.Cmd { cmd := &clibase.Cmd{ - Use: "create [name]", - Short: "Create a template from the current directory or as specified by flag", - Middleware: clibase.Chain( - clibase.RequireRangeArgs(0, 1), - r.InitClient(client), - ), + Use: "create [name]", + Short: "Create a template from the current directory or as specified by flag", + Hidden: true, 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) - - organization, err := CurrentOrganization(inv, client) - if err != nil { - return err - } - - name, err := uploadFlags.templateName(inv.Args) - if err != nil { - return err - } - - job, _, err := createTemplateVersion(createTemplateVersionArgs{ - inv: inv, - client: client, - name: name, - org: organization, - uploadFlags: uploadFlags, - provisionerTags: provisionerTags, - provisioner: provisioner, - variablesFile: variablesFile, - commandLineVariables: commandLineVariables, - versionName: versionName, - alwaysPrompt: false, - }) - if err != nil { - return err - } - - if !uploadFlags.stdin() { - _, err = cliui.Prompt(inv, cliui.PromptOptions{ - Text: "Confirm create?", - IsConfirm: true, - }) - if err != nil { - return err - } - } - - createReq := codersdk.CreateTemplateRequest{ - Name: name, - VersionID: job.ID, - } - - _, err = client.CreateTemplate(inv.Context(), organization.ID, createReq) - if err != nil { - return err - } - - _, _ = fmt.Fprintln(inv.Stdout, "\n"+pretty.Sprint(cliui.DefaultStyles.Wrap, - "The "+pretty.Sprint( - cliui.DefaultStyles.Keyword, name)+" template has been created at "+ - pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp))+"! "+ - "Developers can provision a workspace with this template using:")+"\n") - - _, _ = fmt.Fprintln(inv.Stdout, " "+pretty.Sprint(cliui.DefaultStyles.Code, fmt.Sprintf("coder create --template=%q [workspace name]", name))) - _, _ = fmt.Fprintln(inv.Stdout) - + cliui.DefaultStyles.Error, + "ERROR: The `coder templates create` command has been removed."+ + "Use the `coder templates push` command to create and update templates. "+ + "Use the `coder templates edit` command to change template settings."))) return nil }, } - cmd.Options = clibase.OptionSet{ - { - Flag: "variables-file", - Description: "Specify a file path with values for Terraform-managed variables.", - Value: clibase.StringOf(&variablesFile), - }, - { - Flag: "variable", - Description: "Specify a set of values for Terraform-managed variables.", - Value: clibase.StringArrayOf(&commandLineVariables), - }, - { - Flag: "var", - Description: "Alias of --variable.", - Value: clibase.StringArrayOf(&commandLineVariables), - }, - { - Flag: "provisioner-tag", - Description: "Specify a set of tags to target provisioner daemons.", - Value: clibase.StringArrayOf(&provisionerTags), - }, - { - Flag: "test.provisioner", - Description: "Customize the provisioner backend.", - Default: "terraform", - Value: clibase.StringOf(&provisioner), - Hidden: true, - }, - { - Flag: "name", - Description: "Specify a name for the new template version. It will be automatically generated if not provided.", - Value: clibase.StringOf(&versionName), - }, - cliui.SkipPromptOption(), - } - cmd.Options = append(cmd.Options, uploadFlags.options()...) return cmd } - -type createValidTemplateVersionArgs struct { - Name string - Message string - Client *codersdk.Client - Organization codersdk.Organization - Provisioner codersdk.ProvisionerType - FileID uuid.UUID - - // Template is only required if updating a template's active version. - Template *codersdk.Template - // ReuseParameters will attempt to reuse params from the Template field - // before prompting the user. Set to false to always prompt for param - // values. - ReuseParameters bool - ProvisionerTags map[string]string - UserVariableValues []codersdk.VariableValue -} - -func createValidTemplateVersion(inv *clibase.Invocation, args createValidTemplateVersionArgs) (*codersdk.TemplateVersion, error) { - client := args.Client - - req := codersdk.CreateTemplateVersionRequest{ - Name: args.Name, - Message: args.Message, - StorageMethod: codersdk.ProvisionerStorageMethodFile, - FileID: args.FileID, - Provisioner: args.Provisioner, - ProvisionerTags: args.ProvisionerTags, - UserVariableValues: args.UserVariableValues, - } - if args.Template != nil { - req.TemplateID = args.Template.ID - } - version, err := client.CreateTemplateVersion(inv.Context(), args.Organization.ID, req) - if err != nil { - return nil, err - } - - err = cliui.ProvisionerJob(inv.Context(), inv.Stdout, cliui.ProvisionerJobOptions{ - Fetch: func() (codersdk.ProvisionerJob, error) { - version, err := client.TemplateVersion(inv.Context(), version.ID) - return version.Job, err - }, - Cancel: func() error { - return client.CancelTemplateVersion(inv.Context(), version.ID) - }, - Logs: func() (<-chan codersdk.ProvisionerJobLog, io.Closer, error) { - return client.TemplateVersionLogsAfter(inv.Context(), version.ID, 0) - }, - }) - if err != nil { - var jobErr *cliui.ProvisionerJobError - if errors.As(err, &jobErr) && !codersdk.JobIsMissingParameterErrorCode(jobErr.Code) { - return nil, err - } - if err != nil { - return nil, err - } - } - version, err = client.TemplateVersion(inv.Context(), version.ID) - if err != nil { - return nil, err - } - - if version.Job.Status != codersdk.ProvisionerJobSucceeded { - return nil, xerrors.New(version.Job.Error) - } - - resources, err := client.TemplateVersionResources(inv.Context(), version.ID) - if err != nil { - return nil, err - } - - // Only display the resources on the start transition, to avoid listing them more than once. - var startResources []codersdk.WorkspaceResource - for _, r := range resources { - if r.Transition == codersdk.WorkspaceTransitionStart { - startResources = append(startResources, r) - } - } - err = cliui.WorkspaceResources(inv.Stdout, startResources, cliui.WorkspaceResourcesOptions{ - HideAgentState: true, - HideAccess: true, - Title: "Template Preview", - }) - if err != nil { - return nil, xerrors.Errorf("preview template resources: %w", err) - } - - return &version, nil -} - -func ParseProvisionerTags(rawTags []string) (map[string]string, error) { - tags := map[string]string{} - for _, rawTag := range rawTags { - parts := strings.SplitN(rawTag, "=", 2) - if len(parts) < 2 { - return nil, xerrors.Errorf("invalid tag format for %q. must be key=value", rawTag) - } - tags[parts[0]] = parts[1] - } - return tags, nil -} diff --git a/cli/templatecreate_test.go b/cli/templatecreate_test.go deleted file mode 100644 index 02174f59f7f5a..0000000000000 --- a/cli/templatecreate_test.go +++ /dev/null @@ -1,432 +0,0 @@ -package cli_test - -import ( - "bytes" - "context" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/coder/coder/v2/cli/clitest" - "github.com/coder/coder/v2/coderd/coderdtest" - "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/provisioner/echo" - "github.com/coder/coder/v2/provisionersdk/proto" - "github.com/coder/coder/v2/pty/ptytest" - "github.com/coder/coder/v2/testutil" -) - -func completeWithAgent() *echo.Responses { - return &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionPlan: []*proto.Response{ - { - Type: &proto.Response_Plan{ - Plan: &proto.PlanComplete{ - Resources: []*proto.Resource{ - { - Type: "compute", - Name: "main", - Agents: []*proto.Agent{ - { - Name: "smith", - OperatingSystem: "linux", - Architecture: "i386", - }, - }, - }, - }, - }, - }, - }, - }, - ProvisionApply: []*proto.Response{ - { - Type: &proto.Response_Apply{ - Apply: &proto.ApplyComplete{ - Resources: []*proto.Resource{ - { - Type: "compute", - Name: "main", - Agents: []*proto.Agent{ - { - Name: "smith", - OperatingSystem: "linux", - Architecture: "i386", - }, - }, - }, - }, - }, - }, - }, - }, - } -} - -func TestTemplateCreate(t *testing.T) { - t.Parallel() - t.Run("Create", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - coderdtest.CreateFirstUser(t, client) - source := clitest.CreateTemplateVersionSource(t, completeWithAgent()) - args := []string{ - "templates", - "create", - "my-template", - "--directory", source, - "--test.provisioner", string(database.ProvisionerTypeEcho), - "--default-ttl", "24h", - } - inv, root := clitest.New(t, args...) - clitest.SetupConfig(t, client, root) - pty := ptytest.New(t).Attach(inv) - - clitest.Start(t, inv) - - matches := []struct { - match string - write string - }{ - {match: "Upload", write: "yes"}, - {match: "compute.main"}, - {match: "smith (linux, i386)"}, - {match: "Confirm create?", write: "yes"}, - } - for _, m := range matches { - pty.ExpectMatch(m.match) - if len(m.write) > 0 { - pty.WriteLine(m.write) - } - } - }) - t.Run("CreateNoLockfile", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - coderdtest.CreateFirstUser(t, client) - source := clitest.CreateTemplateVersionSource(t, completeWithAgent()) - require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl"))) - args := []string{ - "templates", - "create", - "my-template", - "--directory", source, - "--test.provisioner", string(database.ProvisionerTypeEcho), - "--default-ttl", "24h", - } - inv, root := clitest.New(t, args...) - clitest.SetupConfig(t, client, root) - pty := ptytest.New(t).Attach(inv) - - execDone := make(chan error) - go func() { - execDone <- inv.Run() - }() - - matches := []struct { - match string - write string - }{ - {match: "No .terraform.lock.hcl file found"}, - {match: "Upload", write: "no"}, - } - for _, m := range matches { - pty.ExpectMatch(m.match) - if len(m.write) > 0 { - pty.WriteLine(m.write) - } - } - - // cmd should error once we say no. - require.Error(t, <-execDone) - }) - t.Run("CreateNoLockfileIgnored", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - coderdtest.CreateFirstUser(t, client) - source := clitest.CreateTemplateVersionSource(t, completeWithAgent()) - require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl"))) - args := []string{ - "templates", - "create", - "my-template", - "--directory", source, - "--test.provisioner", string(database.ProvisionerTypeEcho), - "--default-ttl", "24h", - "--ignore-lockfile", - } - inv, root := clitest.New(t, args...) - clitest.SetupConfig(t, client, root) - pty := ptytest.New(t).Attach(inv) - - execDone := make(chan error) - go func() { - execDone <- inv.Run() - }() - - { - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium) - defer cancel() - - pty.ExpectNoMatchBefore(ctx, "No .terraform.lock.hcl file found", "Upload") - pty.WriteLine("no") - } - - // cmd should error once we say no. - require.Error(t, <-execDone) - }) - - t.Run("CreateStdin", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - coderdtest.CreateFirstUser(t, client) - source, err := echo.Tar(completeWithAgent()) - require.NoError(t, err) - - args := []string{ - "templates", - "create", - "my-template", - "--directory", "-", - "--test.provisioner", string(database.ProvisionerTypeEcho), - "--default-ttl", "24h", - } - inv, root := clitest.New(t, args...) - clitest.SetupConfig(t, client, root) - pty := ptytest.New(t) - inv.Stdin = bytes.NewReader(source) - inv.Stdout = pty.Output() - - require.NoError(t, inv.Run()) - }) - - t.Run("Recreate template with same name (create, delete, create)", func(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - coderdtest.CreateFirstUser(t, client) - - create := func() error { - source := clitest.CreateTemplateVersionSource(t, completeWithAgent()) - args := []string{ - "templates", - "create", - "my-template", - "--yes", - "--directory", source, - "--test.provisioner", string(database.ProvisionerTypeEcho), - } - inv, root := clitest.New(t, args...) - clitest.SetupConfig(t, client, root) - - return inv.Run() - } - del := func() error { - args := []string{ - "templates", - "delete", - "my-template", - "--yes", - } - inv, root := clitest.New(t, args...) - clitest.SetupConfig(t, client, root) - - return inv.Run() - } - - err := create() - require.NoError(t, err, "Template must be created without error") - err = del() - require.NoError(t, err, "Template must be deleted without error") - err = create() - require.NoError(t, err, "Template must be recreated without error") - }) - - t.Run("WithVariablesFileWithoutRequiredValue", func(t *testing.T) { - t.Parallel() - - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - coderdtest.CreateFirstUser(t, client) - - templateVariables := []*proto.TemplateVariable{ - { - Name: "first_variable", - Description: "This is the first variable.", - Type: "string", - Required: true, - Sensitive: true, - }, - { - Name: "second_variable", - Description: "This is the first variable", - Type: "string", - DefaultValue: "abc", - Required: false, - Sensitive: true, - }, - } - source := clitest.CreateTemplateVersionSource(t, - createEchoResponsesWithTemplateVariables(templateVariables)) - tempDir := t.TempDir() - removeTmpDirUntilSuccessAfterTest(t, tempDir) - variablesFile, _ := os.CreateTemp(tempDir, "variables*.yaml") - _, _ = variablesFile.WriteString(`second_variable: foobar`) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) - defer cancel() - - inv, root := clitest.New(t, "templates", "create", "my-template", "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--variables-file", variablesFile.Name()) - clitest.SetupConfig(t, client, root) - inv = inv.WithContext(ctx) - pty := ptytest.New(t).Attach(inv) - - // We expect the cli to return an error, so we have to handle it - // ourselves. - go func() { - cancel() - err := inv.Run() - assert.Error(t, err) - }() - - matches := []struct { - match string - write string - }{ - {match: "Upload", write: "yes"}, - } - for _, m := range matches { - pty.ExpectMatch(m.match) - if len(m.write) > 0 { - pty.WriteLine(m.write) - } - } - - <-ctx.Done() - }) - - t.Run("WithVariablesFileWithTheRequiredValue", func(t *testing.T) { - t.Parallel() - - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - coderdtest.CreateFirstUser(t, client) - - templateVariables := []*proto.TemplateVariable{ - { - Name: "first_variable", - Description: "This is the first variable.", - Type: "string", - Required: true, - Sensitive: true, - }, - { - Name: "second_variable", - Description: "This is the second variable", - Type: "string", - DefaultValue: "abc", - Required: false, - Sensitive: true, - }, - } - source := clitest.CreateTemplateVersionSource(t, - createEchoResponsesWithTemplateVariables(templateVariables)) - tempDir := t.TempDir() - removeTmpDirUntilSuccessAfterTest(t, tempDir) - variablesFile, _ := os.CreateTemp(tempDir, "variables*.yaml") - _, _ = variablesFile.WriteString(`first_variable: foobar`) - inv, root := clitest.New(t, "templates", "create", "my-template", "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--variables-file", variablesFile.Name()) - clitest.SetupConfig(t, client, root) - pty := ptytest.New(t).Attach(inv) - - clitest.Start(t, inv) - - matches := []struct { - match string - write string - }{ - {match: "Upload", write: "yes"}, - {match: "Confirm create?", write: "yes"}, - } - for _, m := range matches { - pty.ExpectMatch(m.match) - if len(m.write) > 0 { - pty.WriteLine(m.write) - } - } - }) - t.Run("WithVariableOption", func(t *testing.T) { - t.Parallel() - - client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) - coderdtest.CreateFirstUser(t, client) - - templateVariables := []*proto.TemplateVariable{ - { - Name: "first_variable", - Description: "This is the first variable.", - Type: "string", - Required: true, - Sensitive: true, - }, - } - source := clitest.CreateTemplateVersionSource(t, - createEchoResponsesWithTemplateVariables(templateVariables)) - inv, root := clitest.New(t, "templates", "create", "my-template", "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--variable", "first_variable=foobar") - clitest.SetupConfig(t, client, root) - pty := ptytest.New(t).Attach(inv) - - clitest.Start(t, inv) - - matches := []struct { - match string - write string - }{ - {match: "Upload", write: "yes"}, - {match: "Confirm create?", write: "yes"}, - } - for _, m := range matches { - pty.ExpectMatch(m.match) - if len(m.write) > 0 { - pty.WriteLine(m.write) - } - } - }) - - t.Run("RequireActiveVersionInvalid", func(t *testing.T) { - t.Parallel() - - client := coderdtest.New(t, &coderdtest.Options{ - IncludeProvisionerDaemon: true, - }) - coderdtest.CreateFirstUser(t, client) - source := clitest.CreateTemplateVersionSource(t, completeWithAgent()) - args := []string{ - "templates", - "create", - "my-template", - "--directory", source, - "--test.provisioner", string(database.ProvisionerTypeEcho), - "--require-active-version", - } - inv, root := clitest.New(t, args...) - clitest.SetupConfig(t, client, root) - - err := inv.Run() - require.Error(t, err) - require.Contains(t, err.Error(), "your deployment appears to be an AGPL deployment, so you cannot set enterprise-only flags") - }) -} - -// Need this for Windows because of a known issue with Go: -// https://github.com/golang/go/issues/52986 -func removeTmpDirUntilSuccessAfterTest(t *testing.T, tempDir string) { - t.Helper() - t.Cleanup(func() { - err := os.RemoveAll(tempDir) - for err != nil { - err = os.RemoveAll(tempDir) - } - }) -} diff --git a/cli/templatepush.go b/cli/templatepush.go index e042addae6134..3ec661cd22299 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -10,17 +10,16 @@ import ( "path/filepath" "strings" "time" - "unicode/utf8" "github.com/briandowns/spinner" + "github.com/google/uuid" "golang.org/x/xerrors" - "github.com/coder/pretty" - "github.com/coder/coder/v2/cli/clibase" "github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/provisionersdk" + "github.com/coder/pretty" ) // templateUploadFlags is shared by `templates create` and `templates push`. @@ -190,24 +189,67 @@ func (r *RootCmd) templatePush() *clibase.Cmd { return err } - job, template, err := createTemplateVersion(createTemplateVersionArgs{ - inv: inv, - client: client, - name: name, - org: organization, - uploadFlags: uploadFlags, - provisionerTags: provisionerTags, - provisioner: provisioner, - variablesFile: variablesFile, - commandLineVariables: commandLineVariables, - versionName: versionName, - alwaysPrompt: alwaysPrompt, - }) + var createTemplate bool + template, err := client.TemplateByName(inv.Context(), organization.ID, name) + if err != nil { + var apiError *codersdk.Error + if errors.As(err, &apiError) && apiError.StatusCode() != http.StatusNotFound { + return err + } + // Template doesn't exist, create it. + createTemplate = true + } + + err = uploadFlags.checkForLockfile(inv) + if err != nil { + return xerrors.Errorf("check for lockfile: %w", err) + } + + message := uploadFlags.templateMessage(inv) + + resp, err := uploadFlags.upload(inv, client) if err != nil { return err } - if template == nil { + tags, err := ParseProvisionerTags(provisionerTags) + if err != nil { + return err + } + + userVariableValues, err := ParseUserVariableValues( + variablesFile, + commandLineVariables) + if err != nil { + return err + } + + args := createValidTemplateVersionArgs{ + Message: message, + Client: client, + Organization: organization, + Provisioner: codersdk.ProvisionerType(provisioner), + FileID: resp.ID, + ProvisionerTags: tags, + UserVariableValues: userVariableValues, + } + + if !createTemplate { + args.Name = versionName + args.Template = &template + args.ReuseParameters = !alwaysPrompt + } + + job, err := createValidTemplateVersion(inv, args) + if err != nil { + return err + } + + if job.Job.Status != codersdk.ProvisionerJobSucceeded { + return xerrors.Errorf("job failed: %s", job.Job.Status) + } + + if createTemplate { _, err = client.CreateTemplate(inv.Context(), organization.ID, codersdk.CreateTemplateRequest{ Name: name, VersionID: job.ID, @@ -230,7 +272,6 @@ func (r *RootCmd) templatePush() *clibase.Cmd { } _, _ = fmt.Fprintf(inv.Stdout, "Updated version at %s!\n", pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp))) - return nil }, } @@ -311,83 +352,106 @@ func prettyDirectoryPath(dir string) string { return prettyDir } -type createTemplateVersionArgs struct { - inv *clibase.Invocation - client *codersdk.Client - name string - org codersdk.Organization - uploadFlags templateUploadFlags - provisionerTags []string - provisioner string - variablesFile string - commandLineVariables []string - versionName string - alwaysPrompt bool +type createValidTemplateVersionArgs struct { + Name string + Message string + Client *codersdk.Client + Organization codersdk.Organization + Provisioner codersdk.ProvisionerType + FileID uuid.UUID + + // Template is only required if updating a template's active version. + Template *codersdk.Template + // ReuseParameters will attempt to reuse params from the Template field + // before prompting the user. Set to false to always prompt for param + // values. + ReuseParameters bool + ProvisionerTags map[string]string + UserVariableValues []codersdk.VariableValue } -func createTemplateVersion(args createTemplateVersionArgs) (*codersdk.TemplateVersion, *codersdk.Template, error) { - if utf8.RuneCountInString(args.name) >= 32 { - return nil, nil, xerrors.Errorf("Template name must be less than 32 characters") +func createValidTemplateVersion(inv *clibase.Invocation, args createValidTemplateVersionArgs) (*codersdk.TemplateVersion, error) { + client := args.Client + + req := codersdk.CreateTemplateVersionRequest{ + Name: args.Name, + Message: args.Message, + StorageMethod: codersdk.ProvisionerStorageMethodFile, + FileID: args.FileID, + Provisioner: args.Provisioner, + ProvisionerTags: args.ProvisionerTags, + UserVariableValues: args.UserVariableValues, } - - var createTemplate bool - template, err := args.client.TemplateByName(args.inv.Context(), args.org.ID, args.name) - if err != nil { - var apiError *codersdk.Error - if errors.As(err, &apiError) && apiError.StatusCode() != http.StatusNotFound { - return nil, nil, err - } - createTemplate = true + if args.Template != nil { + req.TemplateID = args.Template.ID } - - err = args.uploadFlags.checkForLockfile(args.inv) + version, err := client.CreateTemplateVersion(inv.Context(), args.Organization.ID, req) if err != nil { - return nil, nil, xerrors.Errorf("check for lockfile: %w", err) + return nil, err } - message := args.uploadFlags.templateMessage(args.inv) - - resp, err := args.uploadFlags.upload(args.inv, args.client) + err = cliui.ProvisionerJob(inv.Context(), inv.Stdout, cliui.ProvisionerJobOptions{ + Fetch: func() (codersdk.ProvisionerJob, error) { + version, err := client.TemplateVersion(inv.Context(), version.ID) + return version.Job, err + }, + Cancel: func() error { + return client.CancelTemplateVersion(inv.Context(), version.ID) + }, + Logs: func() (<-chan codersdk.ProvisionerJobLog, io.Closer, error) { + return client.TemplateVersionLogsAfter(inv.Context(), version.ID, 0) + }, + }) if err != nil { - return nil, nil, err + var jobErr *cliui.ProvisionerJobError + if errors.As(err, &jobErr) && !codersdk.JobIsMissingParameterErrorCode(jobErr.Code) { + return nil, err + } + if err != nil { + return nil, err + } } - - tags, err := ParseProvisionerTags(args.provisionerTags) + version, err = client.TemplateVersion(inv.Context(), version.ID) if err != nil { - return nil, nil, err + return nil, err } - userVariableValues, err := ParseUserVariableValues( - args.variablesFile, - args.commandLineVariables) - if err != nil { - return nil, nil, err + if version.Job.Status != codersdk.ProvisionerJobSucceeded { + return nil, xerrors.New(version.Job.Error) } - versionArgs := createValidTemplateVersionArgs{ - Message: message, - Client: args.client, - Organization: args.org, - Provisioner: codersdk.ProvisionerType(args.provisioner), - FileID: resp.ID, - ProvisionerTags: tags, - UserVariableValues: userVariableValues, + resources, err := client.TemplateVersionResources(inv.Context(), version.ID) + if err != nil { + return nil, err } - if !createTemplate { - versionArgs.Name = args.versionName - versionArgs.Template = &template - versionArgs.ReuseParameters = !args.alwaysPrompt + // Only display the resources on the start transition, to avoid listing them more than once. + var startResources []codersdk.WorkspaceResource + for _, r := range resources { + if r.Transition == codersdk.WorkspaceTransitionStart { + startResources = append(startResources, r) + } } - - job, err := createValidTemplateVersion(args.inv, versionArgs) + err = cliui.WorkspaceResources(inv.Stdout, startResources, cliui.WorkspaceResourcesOptions{ + HideAgentState: true, + HideAccess: true, + Title: "Template Preview", + }) if err != nil { - return nil, nil, err + return nil, xerrors.Errorf("preview template resources: %w", err) } - if job.Job.Status != codersdk.ProvisionerJobSucceeded { - return nil, nil, xerrors.Errorf("job failed: %s", job.Job.Status) - } + return &version, nil +} - return job, &template, nil +func ParseProvisionerTags(rawTags []string) (map[string]string, error) { + tags := map[string]string{} + for _, rawTag := range rawTags { + parts := strings.SplitN(rawTag, "=", 2) + if len(parts) < 2 { + return nil, xerrors.Errorf("invalid tag format for %q. must be key=value", rawTag) + } + tags[parts[0]] = parts[1] + } + return tags, nil } diff --git a/cli/templatepush_test.go b/cli/templatepush_test.go index 29a43d0524dd3..f945f639c8dc7 100644 --- a/cli/templatepush_test.go +++ b/cli/templatepush_test.go @@ -676,9 +676,10 @@ func TestTemplatePush(t *testing.T) { args := []string{ "templates", "push", - "my-template", + templateName, "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), + "--create", } inv, root := clitest.New(t, args...) clitest.SetupConfig(t, templateAdmin, root) @@ -725,3 +726,63 @@ func createEchoResponsesWithTemplateVariables(templateVariables []*proto.Templat ProvisionApply: echo.ApplyComplete, } } + +func completeWithAgent() *echo.Responses { + return &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: []*proto.Response{ + { + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ + Resources: []*proto.Resource{ + { + Type: "compute", + Name: "main", + Agents: []*proto.Agent{ + { + Name: "smith", + OperatingSystem: "linux", + Architecture: "i386", + }, + }, + }, + }, + }, + }, + }, + }, + ProvisionApply: []*proto.Response{ + { + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ + Resources: []*proto.Resource{ + { + Type: "compute", + Name: "main", + Agents: []*proto.Agent{ + { + Name: "smith", + OperatingSystem: "linux", + Architecture: "i386", + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +// Need this for Windows because of a known issue with Go: +// https://github.com/golang/go/issues/52986 +func removeTmpDirUntilSuccessAfterTest(t *testing.T, tempDir string) { + t.Helper() + t.Cleanup(func() { + err := os.RemoveAll(tempDir) + for err != nil { + err = os.RemoveAll(tempDir) + } + }) +} From 8918cda5daf96cefc1e3d7ef03303e9961e4532e Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 4 Jan 2024 14:22:43 +0000 Subject: [PATCH 25/43] make gen --- cli/testdata/coder_templates_--help.golden | 2 - .../coder_templates_push_--help.golden | 3 - docs/cli/templates.md | 1 - docs/cli/templates_create.md | 140 ------------------ docs/cli/templates_push.md | 128 ---------------- docs/manifest.json | 5 - 6 files changed, 279 deletions(-) delete mode 100644 docs/cli/templates_create.md diff --git a/cli/testdata/coder_templates_--help.golden b/cli/testdata/coder_templates_--help.golden index f9ce76a9ff2c5..7e98b431641b6 100644 --- a/cli/testdata/coder_templates_--help.golden +++ b/cli/testdata/coder_templates_--help.golden @@ -25,8 +25,6 @@ USAGE: SUBCOMMANDS: archive Archive unused or failed template versions from a given template(s) - create Create a template from the current directory or as specified by - flag delete Delete templates edit Edit the metadata of a template by name. init Get started with a templated template. diff --git a/cli/testdata/coder_templates_push_--help.golden b/cli/testdata/coder_templates_push_--help.golden index 9d255c1f8bc23..4e17e00caca47 100644 --- a/cli/testdata/coder_templates_push_--help.golden +++ b/cli/testdata/coder_templates_push_--help.golden @@ -13,9 +13,6 @@ OPTIONS: 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. - -d, --directory string (default: .) Specify the directory to create from, use '-' to read tar from stdin. diff --git a/docs/cli/templates.md b/docs/cli/templates.md index 4a5b60161114f..85253cfc8ed30 100644 --- a/docs/cli/templates.md +++ b/docs/cli/templates.md @@ -36,7 +36,6 @@ Templates are written in standard Terraform and describe the infrastructure for | Name | Purpose | | ------------------------------------------------ | ------------------------------------------------------------------------------ | | [archive](./templates_archive.md) | Archive unused or failed template versions from a given template(s) | -| [create](./templates_create.md) | Create a template from the current directory or as specified by flag | | [delete](./templates_delete.md) | Delete templates | | [edit](./templates_edit.md) | Edit the metadata of a template by name. | | [init](./templates_init.md) | Get started with a templated template. | diff --git a/docs/cli/templates_create.md b/docs/cli/templates_create.md deleted file mode 100644 index 9535e2f12e6da..0000000000000 --- a/docs/cli/templates_create.md +++ /dev/null @@ -1,140 +0,0 @@ - - -# templates create - -Create a template from the current directory or as specified by flag - -## Usage - -```console -coder templates create [flags] [name] -``` - -## Options - -### --default-ttl - -| | | -| ------- | --------------------- | -| Type | 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. - -### -d, --directory - -| | | -| ------- | ------------------- | -| Type | string | -| Default | . | - -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 - -| | | -| ------- | ------------------ | -| Type | bool | -| Default | false | - -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 - -| | | -| ---- | ------------------- | -| Type | string | - -Specify a message describing the changes in this version of the template. Messages longer than 72 characters will be displayed as truncated. - -### --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 - -| | | -| ---- | ------------------------- | -| Type | string-array | - -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 - -| | | -| ---- | ------------------------- | -| Type | string-array | - -Alias of --variable. - -### --variable - -| | | -| ---- | ------------------------- | -| Type | string-array | - -Specify a set of values for Terraform-managed variables. - -### --variables-file - -| | | -| ---- | ------------------- | -| Type | string | - -Specify a file path with values for Terraform-managed variables. - -### -y, --yes - -| | | -| ---- | ----------------- | -| Type | bool | - -Bypass prompts. diff --git a/docs/cli/templates_push.md b/docs/cli/templates_push.md index 9df46fdf69e9c..bac0b971cd76c 100644 --- a/docs/cli/templates_push.md +++ b/docs/cli/templates_push.md @@ -12,14 +12,6 @@ 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 | | | @@ -29,33 +21,6 @@ Sets the template as deprecated. Must be a message explaining why the template i 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 | | | @@ -64,31 +29,6 @@ Allow users to cancel in-progress workspace jobs. 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 - -| | | -| ------- | --------------------- | -| Type | 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 - -| | | -| ---- | ------------------- | -| Type | string | - -Edit the template description. - ### -d, --directory | | | @@ -98,49 +38,6 @@ Edit the template description. 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-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. - -### --icon - -| | | -| ---- | ------------------- | -| Type | string | - -Edit the template icon path. - ### --ignore-lockfile | | | @@ -150,14 +47,6 @@ Edit the template icon path. 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 | | | @@ -174,14 +63,6 @@ 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 | | | @@ -190,15 +71,6 @@ Disable the default behavior of granting template access to the 'everyone' group 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/docs/manifest.json b/docs/manifest.json index 131f0a03f2896..abe17edf01593 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -890,11 +890,6 @@ "description": "Archive unused or failed template versions from a given template(s)", "path": "cli/templates_archive.md" }, - { - "title": "templates create", - "description": "Create a template from the current directory or as specified by flag", - "path": "cli/templates_create.md" - }, { "title": "templates delete", "description": "Delete templates", From 7d2a7accd0b9b1b6a271fae833af906d951df313 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 4 Jan 2024 14:25:35 +0000 Subject: [PATCH 26/43] fix gen --- site/src/api/typesGenerated.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index b38c1b48298eb..17b3091cfe2a5 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1265,7 +1265,6 @@ 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 5b2792d6e661bfb6500a0a049146c78b52c45093 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 4 Jan 2024 14:54:00 +0000 Subject: [PATCH 27/43] add private flag to template edit --- cli/templateedit.go | 9 + cli/templatepush.go | 296 +++++++++--------- coderd/database/queries.sql.go | 19 +- coderd/database/queries/templates.sql | 3 +- coderd/templates.go | 6 + codersdk/templates.go | 6 + docs/cli/templates_edit.md | 9 + site/src/api/typesGenerated.ts | 2 +- .../TemplateSettingsForm.tsx | 1 + .../TemplateSettingsPage.test.tsx | 1 + .../TemplateScheduleForm.tsx | 2 + .../TemplateSchedulePage.test.tsx | 1 + 12 files changed, 199 insertions(+), 156 deletions(-) diff --git a/cli/templateedit.go b/cli/templateedit.go index 9cbcefc88730f..099f31027ac8a 100644 --- a/cli/templateedit.go +++ b/cli/templateedit.go @@ -35,6 +35,7 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { allowUserAutostop bool requireActiveVersion bool deprecationMessage string + disableEveryone bool ) client := new(codersdk.Client) @@ -162,6 +163,7 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { AllowUserAutostop: allowUserAutostop, RequireActiveVersion: requireActiveVersion, DeprecationMessage: deprecated, + DisableEveryoneGroupAccess: disableEveryone, } _, err = client.UpdateTemplateMeta(inv.Context(), template.ID, req) @@ -292,6 +294,13 @@ func (r *RootCmd) templateEdit() *clibase.Cmd { Value: clibase.BoolOf(&requireActiveVersion), Default: "false", }, + { + 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), + Default: "false", + }, cliui.SkipPromptOption(), } diff --git a/cli/templatepush.go b/cli/templatepush.go index 3ec661cd22299..fe5924e339c9e 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strings" "time" + "unicode/utf8" "github.com/briandowns/spinner" "github.com/google/uuid" @@ -22,140 +23,6 @@ import ( "github.com/coder/pretty" ) -// templateUploadFlags is shared by `templates create` and `templates push`. -type templateUploadFlags struct { - directory string - ignoreLockfile bool - message string -} - -func (pf *templateUploadFlags) options() []clibase.Option { - return []clibase.Option{{ - Flag: "directory", - FlagShorthand: "d", - Description: "Specify the directory to create from, use '-' to read tar from stdin.", - Default: ".", - Value: clibase.StringOf(&pf.directory), - }, { - Flag: "ignore-lockfile", - Description: "Ignore warnings about not having a .terraform.lock.hcl file present in the template.", - Default: "false", - Value: clibase.BoolOf(&pf.ignoreLockfile), - }, { - Flag: "message", - FlagShorthand: "m", - Description: "Specify a message describing the changes in this version of the template. Messages longer than 72 characters will be displayed as truncated.", - Value: clibase.StringOf(&pf.message), - }} -} - -func (pf *templateUploadFlags) setWorkdir(wd string) { - if wd == "" { - return - } - if pf.directory == "" || pf.directory == "." { - pf.directory = wd - } else if !filepath.IsAbs(pf.directory) { - pf.directory = filepath.Join(wd, pf.directory) - } -} - -func (pf *templateUploadFlags) stdin() bool { - return pf.directory == "-" -} - -func (pf *templateUploadFlags) upload(inv *clibase.Invocation, client *codersdk.Client) (*codersdk.UploadResponse, error) { - var content io.Reader - if pf.stdin() { - content = inv.Stdin - } else { - prettyDir := prettyDirectoryPath(pf.directory) - _, err := cliui.Prompt(inv, cliui.PromptOptions{ - Text: fmt.Sprintf("Upload %q?", prettyDir), - IsConfirm: true, - Default: cliui.ConfirmYes, - }) - if err != nil { - return nil, err - } - - pipeReader, pipeWriter := io.Pipe() - go func() { - err := provisionersdk.Tar(pipeWriter, inv.Logger, pf.directory, provisionersdk.TemplateArchiveLimit) - _ = pipeWriter.CloseWithError(err) - }() - defer pipeReader.Close() - content = pipeReader - } - - spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond) - spin.Writer = inv.Stdout - spin.Suffix = pretty.Sprint(cliui.DefaultStyles.Keyword, " Uploading directory...") - spin.Start() - defer spin.Stop() - - resp, err := client.Upload(inv.Context(), codersdk.ContentTypeTar, bufio.NewReader(content)) - if err != nil { - return nil, xerrors.Errorf("upload: %w", err) - } - return &resp, nil -} - -func (pf *templateUploadFlags) checkForLockfile(inv *clibase.Invocation) error { - if pf.stdin() || pf.ignoreLockfile { - // Just assume there's a lockfile if reading from stdin. - return nil - } - - hasLockfile, err := provisionersdk.DirHasLockfile(pf.directory) - if err != nil { - return xerrors.Errorf("dir has lockfile: %w", err) - } - - if !hasLockfile { - cliui.Warn(inv.Stdout, "No .terraform.lock.hcl file found", - "When provisioning, Coder will be unable to cache providers without a lockfile and must download them from the internet each time.", - "Create one by running "+pretty.Sprint(cliui.DefaultStyles.Code, "terraform init")+" in your template directory.", - ) - } - return nil -} - -func (pf *templateUploadFlags) templateMessage(inv *clibase.Invocation) string { - title := strings.SplitN(pf.message, "\n", 2)[0] - if len(title) > 72 { - cliui.Warn(inv.Stdout, "Template message is longer than 72 characters, it will be displayed as truncated.") - } - if title != pf.message { - cliui.Warn(inv.Stdout, "Template message contains newlines, only the first line will be displayed.") - } - if pf.message != "" { - return pf.message - } - return "Uploaded from the CLI" -} - -func (pf *templateUploadFlags) templateName(args []string) (string, error) { - if pf.stdin() { - // Can't infer name from directory if none provided. - if len(args) == 0 { - return "", xerrors.New("template name argument must be provided") - } - return args[0], nil - } - - if len(args) > 0 { - return args[0], nil - } - // Have to take absPath to resolve "." and "..". - absPath, err := filepath.Abs(pf.directory) - if err != nil { - return "", err - } - // If no name is provided, use the directory name. - return filepath.Base(absPath), nil -} - func (r *RootCmd) templatePush() *clibase.Cmd { var ( versionName string @@ -189,6 +56,10 @@ func (r *RootCmd) templatePush() *clibase.Cmd { return err } + if utf8.RuneCountInString(name) >= 32 { + 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 { @@ -335,21 +206,137 @@ func (r *RootCmd) templatePush() *clibase.Cmd { return cmd } -// prettyDirectoryPath returns a prettified path when inside the users -// home directory. Falls back to dir if the users home directory cannot -// discerned. This function calls filepath.Clean on the result. -func prettyDirectoryPath(dir string) string { - dir = filepath.Clean(dir) - homeDir, err := os.UserHomeDir() +type templateUploadFlags struct { + directory string + ignoreLockfile bool + message string +} + +func (pf *templateUploadFlags) options() []clibase.Option { + return []clibase.Option{{ + Flag: "directory", + FlagShorthand: "d", + Description: "Specify the directory to create from, use '-' to read tar from stdin.", + Default: ".", + Value: clibase.StringOf(&pf.directory), + }, { + Flag: "ignore-lockfile", + Description: "Ignore warnings about not having a .terraform.lock.hcl file present in the template.", + Default: "false", + Value: clibase.BoolOf(&pf.ignoreLockfile), + }, { + Flag: "message", + FlagShorthand: "m", + Description: "Specify a message describing the changes in this version of the template. Messages longer than 72 characters will be displayed as truncated.", + Value: clibase.StringOf(&pf.message), + }} +} + +func (pf *templateUploadFlags) setWorkdir(wd string) { + if wd == "" { + return + } + if pf.directory == "" || pf.directory == "." { + pf.directory = wd + } else if !filepath.IsAbs(pf.directory) { + pf.directory = filepath.Join(wd, pf.directory) + } +} + +func (pf *templateUploadFlags) stdin() bool { + return pf.directory == "-" +} + +func (pf *templateUploadFlags) upload(inv *clibase.Invocation, client *codersdk.Client) (*codersdk.UploadResponse, error) { + var content io.Reader + if pf.stdin() { + content = inv.Stdin + } else { + prettyDir := prettyDirectoryPath(pf.directory) + _, err := cliui.Prompt(inv, cliui.PromptOptions{ + Text: fmt.Sprintf("Upload %q?", prettyDir), + IsConfirm: true, + Default: cliui.ConfirmYes, + }) + if err != nil { + return nil, err + } + + pipeReader, pipeWriter := io.Pipe() + go func() { + err := provisionersdk.Tar(pipeWriter, inv.Logger, pf.directory, provisionersdk.TemplateArchiveLimit) + _ = pipeWriter.CloseWithError(err) + }() + defer pipeReader.Close() + content = pipeReader + } + + spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond) + spin.Writer = inv.Stdout + spin.Suffix = pretty.Sprint(cliui.DefaultStyles.Keyword, " Uploading directory...") + spin.Start() + defer spin.Stop() + + resp, err := client.Upload(inv.Context(), codersdk.ContentTypeTar, bufio.NewReader(content)) if err != nil { - return dir + return nil, xerrors.Errorf("upload: %w", err) } - prettyDir := dir - if strings.HasPrefix(prettyDir, homeDir) { - prettyDir = strings.TrimPrefix(prettyDir, homeDir) - prettyDir = "~" + prettyDir + return &resp, nil +} + +func (pf *templateUploadFlags) checkForLockfile(inv *clibase.Invocation) error { + if pf.stdin() || pf.ignoreLockfile { + // Just assume there's a lockfile if reading from stdin. + return nil } - return prettyDir + + hasLockfile, err := provisionersdk.DirHasLockfile(pf.directory) + if err != nil { + return xerrors.Errorf("dir has lockfile: %w", err) + } + + if !hasLockfile { + cliui.Warn(inv.Stdout, "No .terraform.lock.hcl file found", + "When provisioning, Coder will be unable to cache providers without a lockfile and must download them from the internet each time.", + "Create one by running "+pretty.Sprint(cliui.DefaultStyles.Code, "terraform init")+" in your template directory.", + ) + } + return nil +} + +func (pf *templateUploadFlags) templateMessage(inv *clibase.Invocation) string { + title := strings.SplitN(pf.message, "\n", 2)[0] + if len(title) > 72 { + cliui.Warn(inv.Stdout, "Template message is longer than 72 characters, it will be displayed as truncated.") + } + if title != pf.message { + cliui.Warn(inv.Stdout, "Template message contains newlines, only the first line will be displayed.") + } + if pf.message != "" { + return pf.message + } + return "Uploaded from the CLI" +} + +func (pf *templateUploadFlags) templateName(args []string) (string, error) { + if pf.stdin() { + // Can't infer name from directory if none provided. + if len(args) == 0 { + return "", xerrors.New("template name argument must be provided") + } + return args[0], nil + } + + if len(args) > 0 { + return args[0], nil + } + // Have to take absPath to resolve "." and "..". + absPath, err := filepath.Abs(pf.directory) + if err != nil { + return "", err + } + // If no name is provided, use the directory name. + return filepath.Base(absPath), nil } type createValidTemplateVersionArgs struct { @@ -455,3 +442,20 @@ func ParseProvisionerTags(rawTags []string) (map[string]string, error) { } return tags, nil } + +// prettyDirectoryPath returns a prettified path when inside the users +// home directory. Falls back to dir if the users home directory cannot +// discerned. This function calls filepath.Clean on the result. +func prettyDirectoryPath(dir string) string { + dir = filepath.Clean(dir) + homeDir, err := os.UserHomeDir() + if err != nil { + return dir + } + prettyDir := dir + if strings.HasPrefix(prettyDir, homeDir) { + prettyDir = strings.TrimPrefix(prettyDir, homeDir) + prettyDir = "~" + prettyDir + } + return prettyDir +} 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_edit.md b/docs/cli/templates_edit.md index 12577cbcaba23..ff73c2828eb83 100644 --- a/docs/cli/templates_edit.md +++ b/docs/cli/templates_edit.md @@ -130,6 +130,15 @@ Edit the template maximum time before shutdown - workspaces created from this te Edit the template name. +### --private + +| | | +| ------- | ------------------ | +| Type | bool | +| Default | false | + +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. + ### --require-active-version | | | diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 17b3091cfe2a5..4bbea6cc062cd 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -809,7 +809,6 @@ export interface ProvisionerDaemon { readonly last_seen_at?: string; readonly name: string; readonly version: string; - readonly api_version: string; readonly provisioners: ProvisionerType[]; readonly tags: Record; } @@ -1265,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 diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx index b402e92a947f2..39f722f59bdb1 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..ab33f72560be3 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx @@ -37,6 +37,7 @@ const validFormValues: TemplateScheduleFormValues = { "saturday", "sunday", ], + disable_everyone_group_access: false, }; const renderTemplateSchedulePage = async () => { From b85a73cf5b796dd0f795d3102512ad6310d60032 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 4 Jan 2024 15:07:37 +0000 Subject: [PATCH 28/43] fix test --- cli/templatepush_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/templatepush_test.go b/cli/templatepush_test.go index f945f639c8dc7..13c9fbc1f35c4 100644 --- a/cli/templatepush_test.go +++ b/cli/templatepush_test.go @@ -679,7 +679,6 @@ func TestTemplatePush(t *testing.T) { templateName, "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), - "--create", } inv, root := clitest.New(t, args...) clitest.SetupConfig(t, templateAdmin, root) From 7655bc9723f6c84f510d152fea5ffcfb5119f3e6 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 4 Jan 2024 15:14:47 +0000 Subject: [PATCH 29/43] fix golden --- cli/testdata/coder_templates_edit_--help.golden | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cli/testdata/coder_templates_edit_--help.golden b/cli/testdata/coder_templates_edit_--help.golden index 94fa1ac45276c..52ef47d363326 100644 --- a/cli/testdata/coder_templates_edit_--help.golden +++ b/cli/testdata/coder_templates_edit_--help.golden @@ -66,6 +66,11 @@ OPTIONS: --name string Edit the template name. + --private bool (default: false) + 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. + --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 From 2f2911a59e84e79444072c8eb66f2136f00b9e87 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 4 Jan 2024 15:31:20 +0000 Subject: [PATCH 30/43] update mentions of templates create command --- .github/workflows/pr-deploy.yaml | 2 +- cli/templateinit.go | 2 +- cli/templatelist.go | 2 +- cli/templates.go | 6 +- docs/admin/provisioners.md | 6 +- docs/cli/templates.md | 7 +- docs/install/openshift.md | 2 +- docs/platforms/azure.md | 2 +- docs/platforms/docker.md | 2 +- .../kubernetes/additional-clusters.md | 4 +- enterprise/cli/templatecreate_test.go | 143 ------------------ examples/examples.gen.json | 2 +- examples/lima/coder.yaml | 2 +- examples/templates/README.md | 2 +- examples/templates/envbox/README.md | 2 +- examples/templates/nomad-docker/README.md | 2 +- scaletest/lib/coder_init.sh | 2 +- scripts/develop.sh | 2 +- 18 files changed, 21 insertions(+), 171 deletions(-) delete mode 100644 enterprise/cli/templatecreate_test.go diff --git a/.github/workflows/pr-deploy.yaml b/.github/workflows/pr-deploy.yaml index 9c657b43ba699..f5045f0bb202a 100644 --- a/.github/workflows/pr-deploy.yaml +++ b/.github/workflows/pr-deploy.yaml @@ -416,7 +416,7 @@ jobs: # Create template cd ./.github/pr-deployments/template - coder templates create -y --variable namespace=pr${{ env.PR_NUMBER }} kubernetes + coder templates push -y --variable namespace=pr${{ env.PR_NUMBER }} kubernetes # Create workspace coder create --template="kubernetes" kube --parameter cpu=2 --parameter memory=4 --parameter home_disk_size=2 -y diff --git a/cli/templateinit.go b/cli/templateinit.go index a9577733bc0fb..db9e3780f1c39 100644 --- a/cli/templateinit.go +++ b/cli/templateinit.go @@ -113,7 +113,7 @@ func (*RootCmd) templateInit() *clibase.Cmd { inv.Stdout, pretty.Sprint( cliui.DefaultStyles.Code, - "cd "+relPath+" && coder templates create"), + "cd "+relPath+" && coder templates push"), ) _, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Wrap, "\nExamples provide a starting point and are expected to be edited! 🎨")) return nil diff --git a/cli/templatelist.go b/cli/templatelist.go index 6d95521dad321..6e18f8462555e 100644 --- a/cli/templatelist.go +++ b/cli/templatelist.go @@ -36,7 +36,7 @@ func (r *RootCmd) templateList() *clibase.Cmd { if len(templates) == 0 { _, _ = fmt.Fprintf(inv.Stderr, "%s No templates found in %s! Create one:\n\n", Caret, color.HiWhiteString(organization.Name)) - _, _ = fmt.Fprintln(inv.Stderr, color.HiMagentaString(" $ coder templates create \n")) + _, _ = fmt.Fprintln(inv.Stderr, color.HiMagentaString(" $ coder templates push \n")) return nil } diff --git a/cli/templates.go b/cli/templates.go index 4f5b4f8f36d0b..71688c04a470e 100644 --- a/cli/templates.go +++ b/cli/templates.go @@ -17,16 +17,12 @@ func (r *RootCmd) templates() *clibase.Cmd { Use: "templates", Short: "Manage templates", Long: "Templates are written in standard Terraform and describe the infrastructure for workspaces\n" + formatExamples( - example{ - Description: "Create a template for developers to create workspaces", - Command: "coder templates create", - }, example{ Description: "Make changes to your template, and plan the changes", Command: "coder templates plan my-template", }, example{ - Description: "Push an update to the template. Your developers can update their workspaces", + Description: "Create or push an update to the template. Your developers can update their workspaces", Command: "coder templates push my-template", }, ), diff --git a/docs/admin/provisioners.md b/docs/admin/provisioners.md index 62a35c1ede1ad..948eba65763f0 100644 --- a/docs/admin/provisioners.md +++ b/docs/admin/provisioners.md @@ -64,11 +64,11 @@ the [Helm example](#example-running-an-external-provisioner-with-helm) below. # In another terminal, create/push # a template that requires this provisioner - coder templates create on-prem \ + coder templates push on-prem \ --provisioner-tag environment=on_prem # Or, match the provisioner exactly - coder templates create on-prem-chicago \ + coder templates push on-prem-chicago \ --provisioner-tag environment=on_prem \ --provisioner-tag data_center=chicago ``` @@ -88,7 +88,7 @@ the [Helm example](#example-running-an-external-provisioner-with-helm) below. # In another terminal, create/push # a template that requires user provisioners - coder templates create on-prem \ + coder templates push on-prem \ --provisioner-tag scope=user ``` diff --git a/docs/cli/templates.md b/docs/cli/templates.md index 85253cfc8ed30..1f9f6a9e1461d 100644 --- a/docs/cli/templates.md +++ b/docs/cli/templates.md @@ -18,15 +18,12 @@ coder templates ```console Templates are written in standard Terraform and describe the infrastructure for workspaces - - Create a template for developers to create workspaces: - - $ coder templates create - - Make changes to your template, and plan the changes: $ coder templates plan my-template - - Push an update to the template. Your developers can update their workspaces: + - Create or push an update to the template. Your developers can update their +workspaces: $ coder templates push my-template ``` diff --git a/docs/install/openshift.md b/docs/install/openshift.md index 7d7440978da24..19e122e47f0a9 100644 --- a/docs/install/openshift.md +++ b/docs/install/openshift.md @@ -322,7 +322,7 @@ Edit `main.tf` and update the following fields of the Kubernetes pod resource: Finally, create the template: ```console -coder template create kubernetes -d . +coder template push kubernetes -d . ``` This template should be ready to use straight away. diff --git a/docs/platforms/azure.md b/docs/platforms/azure.md index 72fab874d3322..df5bb64a5b5fb 100644 --- a/docs/platforms/azure.md +++ b/docs/platforms/azure.md @@ -128,7 +128,7 @@ Navigate to the `./azure-linux` folder where you created your template and run the following command to put the template on your Coder instance. ```shell -coder templates create +coder templates push ``` Congrats! You can now navigate to your Coder dashboard and use this Linux on diff --git a/docs/platforms/docker.md b/docs/platforms/docker.md index 7784e455da570..09e8fc7a4e949 100644 --- a/docs/platforms/docker.md +++ b/docs/platforms/docker.md @@ -52,7 +52,7 @@ Coder with Docker has the following advantages: cd docker ``` -1. Push up the template with `coder templates create` +1. Push up the template with `coder templates push` 1. Open the dashboard in your browser to create your first workspace: diff --git a/docs/platforms/kubernetes/additional-clusters.md b/docs/platforms/kubernetes/additional-clusters.md index 0a27ecb061b35..c3bcd42d18cfe 100644 --- a/docs/platforms/kubernetes/additional-clusters.md +++ b/docs/platforms/kubernetes/additional-clusters.md @@ -211,7 +211,7 @@ export CLUSTER_SERVICEACCOUNT_TOKEN=$(kubectl get secrets coder-v2 -n coder-work Create the template with these values: ```shell -coder templates create \ +coder templates push \ --variable host=$CLUSTER_ADDRESS \ --variable cluster_ca_certificate=$CLUSTER_CA_CERTIFICATE \ --variable token=$CLUSTER_SERVICEACCOUNT_TOKEN \ @@ -228,7 +228,7 @@ kubectl cluster-info # Get cluster CA and token (base64 encoded) kubectl get secrets coder-service-account-token -n coder-workspaces -o jsonpath="{.data}" -coder templates create \ +coder templates push \ --variable host=API_ADDRESS \ --variable cluster_ca_certificate=CLUSTER_CA_CERTIFICATE \ --variable token=CLUSTER_SERVICEACCOUNT_TOKEN \ diff --git a/enterprise/cli/templatecreate_test.go b/enterprise/cli/templatecreate_test.go deleted file mode 100644 index 9499810b7df3a..0000000000000 --- a/enterprise/cli/templatecreate_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package cli_test - -import ( - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/coder/coder/v2/cli/clitest" - "github.com/coder/coder/v2/coderd/coderdtest" - "github.com/coder/coder/v2/coderd/database" - "github.com/coder/coder/v2/coderd/rbac" - "github.com/coder/coder/v2/codersdk" - "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" - "github.com/coder/coder/v2/enterprise/coderd/license" - "github.com/coder/coder/v2/provisioner/echo" - "github.com/coder/coder/v2/testutil" -) - -func TestTemplateCreate(t *testing.T) { - t.Parallel() - - t.Run("RequireActiveVersion", func(t *testing.T) { - t.Parallel() - - client, user := coderdenttest.New(t, &coderdenttest.Options{ - LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{ - codersdk.FeatureAccessControl: 1, - }, - }, - Options: &coderdtest.Options{ - IncludeProvisionerDaemon: true, - }, - }) - templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) - - source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionApply: echo.ApplyComplete, - }) - - inv, conf := newCLI(t, "templates", - "create", "new", - "--directory", source, - "--test.provisioner", string(database.ProvisionerTypeEcho), - "--require-active-version", - "-y", - ) - - clitest.SetupConfig(t, templateAdmin, conf) - - err := inv.Run() - require.NoError(t, err) - - ctx := testutil.Context(t, testutil.WaitMedium) - template, err := templateAdmin.TemplateByName(ctx, user.OrganizationID, "new") - require.NoError(t, err) - require.True(t, template.RequireActiveVersion) - }) - - t.Run("WorkspaceCleanup", func(t *testing.T) { - t.Parallel() - - dv := coderdtest.DeploymentValues(t) - dv.Experiments = []string{ - string(codersdk.ExperimentWorkspaceActions), - } - - client, user := coderdenttest.New(t, &coderdenttest.Options{ - LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{ - codersdk.FeatureAdvancedTemplateScheduling: 1, - }, - }, - Options: &coderdtest.Options{ - DeploymentValues: dv, - IncludeProvisionerDaemon: true, - }, - }) - templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) - - source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionApply: echo.ApplyComplete, - }) - - const ( - expectedFailureTTL = time.Hour * 3 - expectedDormancyThreshold = time.Hour * 4 - expectedDormancyAutoDeletion = time.Minute * 10 - ) - - inv, conf := newCLI(t, "templates", - "create", "new", - "--directory", source, - "--test.provisioner", string(database.ProvisionerTypeEcho), - "--failure-ttl="+expectedFailureTTL.String(), - "--dormancy-threshold="+expectedDormancyThreshold.String(), - "--dormancy-auto-deletion="+expectedDormancyAutoDeletion.String(), - "-y", - "--", - ) - - clitest.SetupConfig(t, templateAdmin, conf) - - err := inv.Run() - require.NoError(t, err) - - ctx := testutil.Context(t, testutil.WaitMedium) - template, err := templateAdmin.TemplateByName(ctx, user.OrganizationID, "new") - require.NoError(t, err) - require.Equal(t, expectedFailureTTL.Milliseconds(), template.FailureTTLMillis) - require.Equal(t, expectedDormancyThreshold.Milliseconds(), template.TimeTilDormantMillis) - require.Equal(t, expectedDormancyAutoDeletion.Milliseconds(), template.TimeTilDormantAutoDeleteMillis) - }) - - t.Run("NotEntitled", func(t *testing.T) { - t.Parallel() - - client, admin := coderdenttest.New(t, &coderdenttest.Options{ - LicenseOptions: &coderdenttest.LicenseOptions{ - Features: license.Features{}, - }, - Options: &coderdtest.Options{ - IncludeProvisionerDaemon: true, - }, - }) - templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleTemplateAdmin()) - - inv, conf := newCLI(t, "templates", - "create", "new", - "--require-active-version", - "-y", - ) - - clitest.SetupConfig(t, templateAdmin, conf) - - err := inv.Run() - require.Error(t, err) - require.Contains(t, err.Error(), "your license is not entitled to use enterprise access control, so you cannot set --require-active-version") - }) -} diff --git a/examples/examples.gen.json b/examples/examples.gen.json index d216581c7c116..ea2ec0abc10d8 100644 --- a/examples/examples.gen.json +++ b/examples/examples.gen.json @@ -155,6 +155,6 @@ "nomad", "container" ], - "markdown": "\n# Remote Development on Nomad\n\nProvision Nomad Jobs as [Coder workspaces](https://coder.com/docs/coder-v2/latest) with this example template. This example shows how to use Nomad service tasks to be used as a development environment using docker and host csi volumes.\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## Prerequisites\n\n- [Nomad](https://www.nomadproject.io/downloads)\n- [Docker](https://docs.docker.com/get-docker/)\n\n## Setup\n\n### 1. Start the CSI Host Volume Plugin\n\nThe CSI Host Volume plugin is used to mount host volumes into Nomad tasks. This is useful for development environments where you want to mount persistent volumes into your container workspace.\n\n1. Login to the Nomad server using SSH.\n\n2. Append the following stanza to your Nomad server configuration file and restart the nomad service.\n\n ```hcl\n plugin \"docker\" {\n config {\n allow_privileged = true\n }\n }\n ```\n\n ```shell\n sudo systemctl restart nomad\n ```\n\n3. Create a file `hostpath.nomad` with following content:\n\n ```hcl\n job \"hostpath-csi-plugin\" {\n datacenters = [\"dc1\"]\n type = \"system\"\n\n group \"csi\" {\n task \"plugin\" {\n driver = \"docker\"\n\n config {\n image = \"registry.k8s.io/sig-storage/hostpathplugin:v1.10.0\"\n\n args = [\n \"--drivername=csi-hostpath\",\n \"--v=5\",\n \"--endpoint=${CSI_ENDPOINT}\",\n \"--nodeid=node-${NOMAD_ALLOC_INDEX}\",\n ]\n\n privileged = true\n }\n\n csi_plugin {\n id = \"hostpath\"\n type = \"monolith\"\n mount_dir = \"/csi\"\n }\n\n resources {\n cpu = 256\n memory = 128\n }\n }\n }\n }\n ```\n\n4. Run the job:\n\n ```shell\n nomad job run hostpath.nomad\n ```\n\n### 2. Setup the Nomad Template\n\n1. Create the template by running the following command:\n\n ```shell\n coder template init nomad-docker\n cd nomad-docker\n coder template create\n ```\n\n2. Set up Nomad server address and optional authentication:\n\n3. Create a new workspace and start developing.\n" + "markdown": "\n# Remote Development on Nomad\n\nProvision Nomad Jobs as [Coder workspaces](https://coder.com/docs/coder-v2/latest) with this example template. This example shows how to use Nomad service tasks to be used as a development environment using docker and host csi volumes.\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## Prerequisites\n\n- [Nomad](https://www.nomadproject.io/downloads)\n- [Docker](https://docs.docker.com/get-docker/)\n\n## Setup\n\n### 1. Start the CSI Host Volume Plugin\n\nThe CSI Host Volume plugin is used to mount host volumes into Nomad tasks. This is useful for development environments where you want to mount persistent volumes into your container workspace.\n\n1. Login to the Nomad server using SSH.\n\n2. Append the following stanza to your Nomad server configuration file and restart the nomad service.\n\n ```hcl\n plugin \"docker\" {\n config {\n allow_privileged = true\n }\n }\n ```\n\n ```shell\n sudo systemctl restart nomad\n ```\n\n3. Create a file `hostpath.nomad` with following content:\n\n ```hcl\n job \"hostpath-csi-plugin\" {\n datacenters = [\"dc1\"]\n type = \"system\"\n\n group \"csi\" {\n task \"plugin\" {\n driver = \"docker\"\n\n config {\n image = \"registry.k8s.io/sig-storage/hostpathplugin:v1.10.0\"\n\n args = [\n \"--drivername=csi-hostpath\",\n \"--v=5\",\n \"--endpoint=${CSI_ENDPOINT}\",\n \"--nodeid=node-${NOMAD_ALLOC_INDEX}\",\n ]\n\n privileged = true\n }\n\n csi_plugin {\n id = \"hostpath\"\n type = \"monolith\"\n mount_dir = \"/csi\"\n }\n\n resources {\n cpu = 256\n memory = 128\n }\n }\n }\n }\n ```\n\n4. Run the job:\n\n ```shell\n nomad job run hostpath.nomad\n ```\n\n### 2. Setup the Nomad Template\n\n1. Create the template by running the following command:\n\n ```shell\n coder template init nomad-docker\n cd nomad-docker\n coder template push\n ```\n\n2. Set up Nomad server address and optional authentication:\n\n3. Create a new workspace and start developing.\n" } ] diff --git a/examples/lima/coder.yaml b/examples/lima/coder.yaml index bb0f1528b8cfc..f9b8a1176e347 100644 --- a/examples/lima/coder.yaml +++ b/examples/lima/coder.yaml @@ -103,7 +103,7 @@ provision: fi DOCKER_HOST=$(docker context inspect --format '{{.Endpoints.docker.Host}}') printf 'docker_arch: "%s"\ndocker_host: "%s"\n' "${DOCKER_ARCH}" "${DOCKER_HOST}" | tee "${temp_template_dir}/params.yaml" - coder templates create "docker-${DOCKER_ARCH}" --directory "${temp_template_dir}" --variables-file "${temp_template_dir}/params.yaml" --yes + coder templates push "docker-${DOCKER_ARCH}" --directory "${temp_template_dir}" --variables-file "${temp_template_dir}/params.yaml" --yes rm -rfv "${temp_template_dir}" probes: - description: "docker to be installed" diff --git a/examples/templates/README.md b/examples/templates/README.md index 38ade2345d70f..3ab46a52ad41f 100644 --- a/examples/templates/README.md +++ b/examples/templates/README.md @@ -11,7 +11,7 @@ Clone this repository to create a template from any example listed here: ```console git clone https://github.com/coder/coder cd examples/templates/aws-linux -coder templates create +coder templates push ``` ## Community Templates diff --git a/examples/templates/envbox/README.md b/examples/templates/envbox/README.md index d5632294d63d1..ad97f7777edad 100644 --- a/examples/templates/envbox/README.md +++ b/examples/templates/envbox/README.md @@ -47,7 +47,7 @@ To supply values to existing existing Terraform variables you can specify the `-V` flag. For example ```bash -coder templates create envbox --var namespace="mynamespace" --var max_cpus=2 --var min_cpus=1 --var max_memory=4 --var min_memory=1 +coder templates push envbox --var namespace="mynamespace" --var max_cpus=2 --var min_cpus=1 --var max_memory=4 --var min_memory=1 ``` ## Contributions diff --git a/examples/templates/nomad-docker/README.md b/examples/templates/nomad-docker/README.md index b5ce5344837da..17310ae2e9852 100644 --- a/examples/templates/nomad-docker/README.md +++ b/examples/templates/nomad-docker/README.md @@ -95,7 +95,7 @@ The CSI Host Volume plugin is used to mount host volumes into Nomad tasks. This ```shell coder template init nomad-docker cd nomad-docker - coder template create + coder template push ``` 2. Set up Nomad server address and optional authentication: diff --git a/scaletest/lib/coder_init.sh b/scaletest/lib/coder_init.sh index f8c905958ece4..4b8ea10986b7c 100755 --- a/scaletest/lib/coder_init.sh +++ b/scaletest/lib/coder_init.sh @@ -68,7 +68,7 @@ CODER_FIRST_USER_TRIAL="${CODER_FIRST_USER_TRIAL}" EOF echo "Importing kubernetes template" -DRY_RUN="$DRY_RUN" "$PROJECT_ROOT/scaletest/lib/coder_shim.sh" templates create \ +DRY_RUN="$DRY_RUN" "$PROJECT_ROOT/scaletest/lib/coder_shim.sh" templates push \ --global-config="${CONFIG_DIR}" \ --directory "${CONFIG_DIR}/templates/kubernetes" \ --yes kubernetes diff --git a/scripts/develop.sh b/scripts/develop.sh index 39f81c2951bc4..ba5116f5a7735 100755 --- a/scripts/develop.sh +++ b/scripts/develop.sh @@ -177,7 +177,7 @@ fatal() { DOCKER_HOST="$(docker context inspect --format '{{ .Endpoints.docker.Host }}')" printf 'docker_arch: "%s"\ndocker_host: "%s"\n' "${GOARCH}" "${DOCKER_HOST}" >"${temp_template_dir}/params.yaml" ( - "${CODER_DEV_SHIM}" templates create "${template_name}" --directory "${temp_template_dir}" --variables-file "${temp_template_dir}/params.yaml" --yes + "${CODER_DEV_SHIM}" templates push "${template_name}" --directory "${temp_template_dir}" --variables-file "${temp_template_dir}/params.yaml" --yes rm -rfv "${temp_template_dir}" # Only delete template dir if template creation succeeds ) || echo "Failed to create a template. The template files are in ${temp_template_dir}" fi From 5334e8edcf1e3c33fb6d40f2b05c53cf23b5a151 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 4 Jan 2024 15:33:01 +0000 Subject: [PATCH 31/43] update golden --- cli/testdata/coder_templates_--help.golden | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cli/testdata/coder_templates_--help.golden b/cli/testdata/coder_templates_--help.golden index 7e98b431641b6..44c301f20f1e1 100644 --- a/cli/testdata/coder_templates_--help.golden +++ b/cli/testdata/coder_templates_--help.golden @@ -9,15 +9,11 @@ USAGE: Templates are written in standard Terraform and describe the infrastructure for workspaces - - Create a template for developers to create workspaces: - - $ coder templates create - - Make changes to your template, and plan the changes: $ coder templates plan my-template - - Push an update to the template. Your developers can update their + - Create or push an update to the template. Your developers can update their workspaces: $ coder templates push my-template From 79c0c025f2d8bbd08a4818f046286476937c5631 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 4 Jan 2024 15:35:13 +0000 Subject: [PATCH 32/43] add removal comment --- cli/templatecreate.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/templatecreate.go b/cli/templatecreate.go index cafae32712f10..d9e5a15ec51de 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -8,6 +8,7 @@ import ( "github.com/coder/pretty" ) +// TODO(f0ssel): This should be removed a few versions after coder 2.7.0 has been released. func (*RootCmd) templateCreate() *clibase.Cmd { cmd := &clibase.Cmd{ Use: "create [name]", @@ -17,7 +18,7 @@ func (*RootCmd) templateCreate() *clibase.Cmd { _, _ = fmt.Fprintln(inv.Stdout, "\n"+pretty.Sprint(cliui.DefaultStyles.Wrap, pretty.Sprint( cliui.DefaultStyles.Error, - "ERROR: The `coder templates create` command has been removed."+ + "ERROR: The `coder templates create` command has been removed. "+ "Use the `coder templates push` command to create and update templates. "+ "Use the `coder templates edit` command to change template settings."))) return nil From 2dfb98e280d7ed4ee2af31e330aeba9cdc17c643 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 4 Jan 2024 19:26:49 +0000 Subject: [PATCH 33/43] Add back create --- cli/templatecreate.go | 251 +++++++++++++- cli/templatecreate_test.go | 372 +++++++++++++++++++++ cli/testdata/coder_templates_--help.golden | 2 + docs/cli/templates.md | 1 + docs/cli/templates_create.md | 140 ++++++++ docs/manifest.json | 5 + 6 files changed, 761 insertions(+), 10 deletions(-) create mode 100644 cli/templatecreate_test.go create mode 100644 docs/cli/templates_create.md diff --git a/cli/templatecreate.go b/cli/templatecreate.go index d9e5a15ec51de..87f138514416d 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -2,28 +2,259 @@ package cli import ( "fmt" + "net/http" + "time" + "unicode/utf8" + + "golang.org/x/xerrors" + + "github.com/coder/pretty" "github.com/coder/coder/v2/cli/clibase" "github.com/coder/coder/v2/cli/cliui" - "github.com/coder/pretty" + "github.com/coder/coder/v2/coderd/util/ptr" + "github.com/coder/coder/v2/codersdk" ) -// TODO(f0ssel): This should be removed a few versions after coder 2.7.0 has been released. -func (*RootCmd) templateCreate() *clibase.Cmd { +func (r *RootCmd) templateCreate() *clibase.Cmd { + var ( + provisioner string + provisionerTags []string + variablesFile string + commandLineVariables []string + disableEveryone bool + requireActiveVersion bool + + defaultTTL time.Duration + failureTTL time.Duration + dormancyThreshold time.Duration + dormancyAutoDeletion time.Duration + maxTTL time.Duration + + uploadFlags templateUploadFlags + ) + client := new(codersdk.Client) cmd := &clibase.Cmd{ - Use: "create [name]", - Short: "Create a template from the current directory or as specified by flag", - Hidden: true, + Use: "create [name]", + Short: "Create a template from the current directory or as specified by flag", + Middleware: clibase.Chain( + clibase.RequireRangeArgs(0, 1), + r.InitClient(client), + ), Handler: func(inv *clibase.Invocation) error { _, _ = fmt.Fprintln(inv.Stdout, "\n"+pretty.Sprint(cliui.DefaultStyles.Wrap, pretty.Sprint( - cliui.DefaultStyles.Error, - "ERROR: The `coder templates create` command has been removed. "+ - "Use the `coder templates push` command to create and update templates. "+ - "Use the `coder templates edit` command to change template settings."))) + cliui.DefaultStyles.Warn, + "DEPRECATION WARNING: Use `coder templates push` command for creating and updating templates. "+ + "Use `coder templates edit` command for editing template settings."+ + "This command will be removed in a future release."+ + "Waiting 1 second...")+"\n")) + time.Sleep(1 * time.Second) + + 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") + } + } + } + + organization, err := CurrentOrganization(inv, client) + if err != nil { + return err + } + + templateName, err := uploadFlags.templateName(inv.Args) + if err != nil { + return err + } + + if utf8.RuneCountInString(templateName) > 31 { + return xerrors.Errorf("Template name must be less than 32 characters") + } + + _, err = client.TemplateByName(inv.Context(), organization.ID, templateName) + if err == nil { + return xerrors.Errorf("A template already exists named %q!", templateName) + } + + err = uploadFlags.checkForLockfile(inv) + if err != nil { + return xerrors.Errorf("check for lockfile: %w", err) + } + + message := uploadFlags.templateMessage(inv) + + // Confirm upload of the directory. + resp, err := uploadFlags.upload(inv, client) + if err != nil { + return err + } + + tags, err := ParseProvisionerTags(provisionerTags) + if err != nil { + return err + } + + userVariableValues, err := ParseUserVariableValues( + variablesFile, + commandLineVariables) + if err != nil { + return err + } + + job, err := createValidTemplateVersion(inv, createValidTemplateVersionArgs{ + Message: message, + Client: client, + Organization: organization, + Provisioner: codersdk.ProvisionerType(provisioner), + FileID: resp.ID, + ProvisionerTags: tags, + UserVariableValues: userVariableValues, + }) + if err != nil { + return err + } + + if !uploadFlags.stdin() { + _, err = cliui.Prompt(inv, cliui.PromptOptions{ + Text: "Confirm create?", + IsConfirm: true, + }) + if err != nil { + return err + } + } + + createReq := codersdk.CreateTemplateRequest{ + Name: templateName, + VersionID: job.ID, + DefaultTTLMillis: ptr.Ref(defaultTTL.Milliseconds()), + FailureTTLMillis: ptr.Ref(failureTTL.Milliseconds()), + MaxTTLMillis: ptr.Ref(maxTTL.Milliseconds()), + TimeTilDormantMillis: ptr.Ref(dormancyThreshold.Milliseconds()), + TimeTilDormantAutoDeleteMillis: ptr.Ref(dormancyAutoDeletion.Milliseconds()), + DisableEveryoneGroupAccess: disableEveryone, + RequireActiveVersion: requireActiveVersion, + } + + _, err = client.CreateTemplate(inv.Context(), organization.ID, createReq) + if err != nil { + return err + } + + _, _ = fmt.Fprintln(inv.Stdout, "\n"+pretty.Sprint(cliui.DefaultStyles.Wrap, + "The "+pretty.Sprint( + cliui.DefaultStyles.Keyword, templateName)+" template has been created at "+ + pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp))+"! "+ + "Developers can provision a workspace with this template using:")+"\n") + + _, _ = fmt.Fprintln(inv.Stdout, " "+pretty.Sprint(cliui.DefaultStyles.Code, fmt.Sprintf("coder create --template=%q [workspace name]", templateName))) + _, _ = fmt.Fprintln(inv.Stdout) + return nil }, } + cmd.Options = clibase.OptionSet{ + { + 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), + }, + { + Flag: "variables-file", + Description: "Specify a file path with values for Terraform-managed variables.", + Value: clibase.StringOf(&variablesFile), + }, + { + Flag: "variable", + Description: "Specify a set of values for Terraform-managed variables.", + Value: clibase.StringArrayOf(&commandLineVariables), + }, + { + Flag: "var", + Description: "Alias of --variable.", + Value: clibase.StringArrayOf(&commandLineVariables), + }, + { + Flag: "provisioner-tag", + Description: "Specify a set of tags to target provisioner daemons.", + Value: clibase.StringArrayOf(&provisionerTags), + }, + { + 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: "test.provisioner", + Description: "Customize the provisioner backend.", + Default: "terraform", + Value: clibase.StringOf(&provisioner), + Hidden: true, + }, + { + 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", + }, + + cliui.SkipPromptOption(), + } + cmd.Options = append(cmd.Options, uploadFlags.options()...) return cmd } diff --git a/cli/templatecreate_test.go b/cli/templatecreate_test.go new file mode 100644 index 0000000000000..0eaf1344ea298 --- /dev/null +++ b/cli/templatecreate_test.go @@ -0,0 +1,372 @@ +package cli_test + +import ( + "bytes" + "context" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/cli/clitest" + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/provisioner/echo" + "github.com/coder/coder/v2/provisionersdk/proto" + "github.com/coder/coder/v2/pty/ptytest" + "github.com/coder/coder/v2/testutil" +) + +func TestTemplateCreate(t *testing.T) { + t.Parallel() + t.Run("Create", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + coderdtest.CreateFirstUser(t, client) + source := clitest.CreateTemplateVersionSource(t, completeWithAgent()) + args := []string{ + "templates", + "create", + "my-template", + "--directory", source, + "--test.provisioner", string(database.ProvisionerTypeEcho), + "--default-ttl", "24h", + } + inv, root := clitest.New(t, args...) + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t).Attach(inv) + + clitest.Start(t, inv) + + matches := []struct { + match string + write string + }{ + {match: "Upload", write: "yes"}, + {match: "compute.main"}, + {match: "smith (linux, i386)"}, + {match: "Confirm create?", write: "yes"}, + } + for _, m := range matches { + pty.ExpectMatch(m.match) + if len(m.write) > 0 { + pty.WriteLine(m.write) + } + } + }) + t.Run("CreateNoLockfile", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + coderdtest.CreateFirstUser(t, client) + source := clitest.CreateTemplateVersionSource(t, completeWithAgent()) + require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl"))) + args := []string{ + "templates", + "create", + "my-template", + "--directory", source, + "--test.provisioner", string(database.ProvisionerTypeEcho), + "--default-ttl", "24h", + } + inv, root := clitest.New(t, args...) + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t).Attach(inv) + + execDone := make(chan error) + go func() { + execDone <- inv.Run() + }() + + matches := []struct { + match string + write string + }{ + {match: "No .terraform.lock.hcl file found"}, + {match: "Upload", write: "no"}, + } + for _, m := range matches { + pty.ExpectMatch(m.match) + if len(m.write) > 0 { + pty.WriteLine(m.write) + } + } + + // cmd should error once we say no. + require.Error(t, <-execDone) + }) + t.Run("CreateNoLockfileIgnored", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + coderdtest.CreateFirstUser(t, client) + source := clitest.CreateTemplateVersionSource(t, completeWithAgent()) + require.NoError(t, os.Remove(filepath.Join(source, ".terraform.lock.hcl"))) + args := []string{ + "templates", + "create", + "my-template", + "--directory", source, + "--test.provisioner", string(database.ProvisionerTypeEcho), + "--default-ttl", "24h", + "--ignore-lockfile", + } + inv, root := clitest.New(t, args...) + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t).Attach(inv) + + execDone := make(chan error) + go func() { + execDone <- inv.Run() + }() + + { + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium) + defer cancel() + + pty.ExpectNoMatchBefore(ctx, "No .terraform.lock.hcl file found", "Upload") + pty.WriteLine("no") + } + + // cmd should error once we say no. + require.Error(t, <-execDone) + }) + + t.Run("CreateStdin", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + coderdtest.CreateFirstUser(t, client) + source, err := echo.Tar(completeWithAgent()) + require.NoError(t, err) + + args := []string{ + "templates", + "create", + "my-template", + "--directory", "-", + "--test.provisioner", string(database.ProvisionerTypeEcho), + "--default-ttl", "24h", + } + inv, root := clitest.New(t, args...) + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t) + inv.Stdin = bytes.NewReader(source) + inv.Stdout = pty.Output() + + require.NoError(t, inv.Run()) + }) + + t.Run("Recreate template with same name (create, delete, create)", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + coderdtest.CreateFirstUser(t, client) + + create := func() error { + source := clitest.CreateTemplateVersionSource(t, completeWithAgent()) + args := []string{ + "templates", + "create", + "my-template", + "--yes", + "--directory", source, + "--test.provisioner", string(database.ProvisionerTypeEcho), + } + inv, root := clitest.New(t, args...) + clitest.SetupConfig(t, client, root) + + return inv.Run() + } + del := func() error { + args := []string{ + "templates", + "delete", + "my-template", + "--yes", + } + inv, root := clitest.New(t, args...) + clitest.SetupConfig(t, client, root) + + return inv.Run() + } + + err := create() + require.NoError(t, err, "Template must be created without error") + err = del() + require.NoError(t, err, "Template must be deleted without error") + err = create() + require.NoError(t, err, "Template must be recreated without error") + }) + + t.Run("WithVariablesFileWithoutRequiredValue", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + coderdtest.CreateFirstUser(t, client) + + templateVariables := []*proto.TemplateVariable{ + { + Name: "first_variable", + Description: "This is the first variable.", + Type: "string", + Required: true, + Sensitive: true, + }, + { + Name: "second_variable", + Description: "This is the first variable", + Type: "string", + DefaultValue: "abc", + Required: false, + Sensitive: true, + }, + } + source := clitest.CreateTemplateVersionSource(t, + createEchoResponsesWithTemplateVariables(templateVariables)) + tempDir := t.TempDir() + removeTmpDirUntilSuccessAfterTest(t, tempDir) + variablesFile, _ := os.CreateTemp(tempDir, "variables*.yaml") + _, _ = variablesFile.WriteString(`second_variable: foobar`) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + inv, root := clitest.New(t, "templates", "create", "my-template", "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--variables-file", variablesFile.Name()) + clitest.SetupConfig(t, client, root) + inv = inv.WithContext(ctx) + pty := ptytest.New(t).Attach(inv) + + // We expect the cli to return an error, so we have to handle it + // ourselves. + go func() { + cancel() + err := inv.Run() + assert.Error(t, err) + }() + + matches := []struct { + match string + write string + }{ + {match: "Upload", write: "yes"}, + } + for _, m := range matches { + pty.ExpectMatch(m.match) + if len(m.write) > 0 { + pty.WriteLine(m.write) + } + } + + <-ctx.Done() + }) + + t.Run("WithVariablesFileWithTheRequiredValue", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + coderdtest.CreateFirstUser(t, client) + + templateVariables := []*proto.TemplateVariable{ + { + Name: "first_variable", + Description: "This is the first variable.", + Type: "string", + Required: true, + Sensitive: true, + }, + { + Name: "second_variable", + Description: "This is the second variable", + Type: "string", + DefaultValue: "abc", + Required: false, + Sensitive: true, + }, + } + source := clitest.CreateTemplateVersionSource(t, + createEchoResponsesWithTemplateVariables(templateVariables)) + tempDir := t.TempDir() + removeTmpDirUntilSuccessAfterTest(t, tempDir) + variablesFile, _ := os.CreateTemp(tempDir, "variables*.yaml") + _, _ = variablesFile.WriteString(`first_variable: foobar`) + inv, root := clitest.New(t, "templates", "create", "my-template", "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--variables-file", variablesFile.Name()) + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t).Attach(inv) + + clitest.Start(t, inv) + + matches := []struct { + match string + write string + }{ + {match: "Upload", write: "yes"}, + {match: "Confirm create?", write: "yes"}, + } + for _, m := range matches { + pty.ExpectMatch(m.match) + if len(m.write) > 0 { + pty.WriteLine(m.write) + } + } + }) + t.Run("WithVariableOption", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + coderdtest.CreateFirstUser(t, client) + + templateVariables := []*proto.TemplateVariable{ + { + Name: "first_variable", + Description: "This is the first variable.", + Type: "string", + Required: true, + Sensitive: true, + }, + } + source := clitest.CreateTemplateVersionSource(t, + createEchoResponsesWithTemplateVariables(templateVariables)) + inv, root := clitest.New(t, "templates", "create", "my-template", "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--variable", "first_variable=foobar") + clitest.SetupConfig(t, client, root) + pty := ptytest.New(t).Attach(inv) + + clitest.Start(t, inv) + + matches := []struct { + match string + write string + }{ + {match: "Upload", write: "yes"}, + {match: "Confirm create?", write: "yes"}, + } + for _, m := range matches { + pty.ExpectMatch(m.match) + if len(m.write) > 0 { + pty.WriteLine(m.write) + } + } + }) + + t.Run("RequireActiveVersionInvalid", func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }) + coderdtest.CreateFirstUser(t, client) + source := clitest.CreateTemplateVersionSource(t, completeWithAgent()) + args := []string{ + "templates", + "create", + "my-template", + "--directory", source, + "--test.provisioner", string(database.ProvisionerTypeEcho), + "--require-active-version", + } + inv, root := clitest.New(t, args...) + clitest.SetupConfig(t, client, root) + + err := inv.Run() + require.Error(t, err) + require.Contains(t, err.Error(), "your deployment appears to be an AGPL deployment, so you cannot set enterprise-only flags") + }) +} diff --git a/cli/testdata/coder_templates_--help.golden b/cli/testdata/coder_templates_--help.golden index 44c301f20f1e1..2244a6b787c42 100644 --- a/cli/testdata/coder_templates_--help.golden +++ b/cli/testdata/coder_templates_--help.golden @@ -21,6 +21,8 @@ USAGE: SUBCOMMANDS: archive Archive unused or failed template versions from a given template(s) + create Create a template from the current directory or as specified by + flag delete Delete templates edit Edit the metadata of a template by name. init Get started with a templated template. diff --git a/docs/cli/templates.md b/docs/cli/templates.md index 1f9f6a9e1461d..8a39938296584 100644 --- a/docs/cli/templates.md +++ b/docs/cli/templates.md @@ -33,6 +33,7 @@ workspaces: | Name | Purpose | | ------------------------------------------------ | ------------------------------------------------------------------------------ | | [archive](./templates_archive.md) | Archive unused or failed template versions from a given template(s) | +| [create](./templates_create.md) | Create a template from the current directory or as specified by flag | | [delete](./templates_delete.md) | Delete templates | | [edit](./templates_edit.md) | Edit the metadata of a template by name. | | [init](./templates_init.md) | Get started with a templated template. | diff --git a/docs/cli/templates_create.md b/docs/cli/templates_create.md new file mode 100644 index 0000000000000..9535e2f12e6da --- /dev/null +++ b/docs/cli/templates_create.md @@ -0,0 +1,140 @@ + + +# templates create + +Create a template from the current directory or as specified by flag + +## Usage + +```console +coder templates create [flags] [name] +``` + +## Options + +### --default-ttl + +| | | +| ------- | --------------------- | +| Type | 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. + +### -d, --directory + +| | | +| ------- | ------------------- | +| Type | string | +| Default | . | + +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 + +| | | +| ------- | ------------------ | +| Type | bool | +| Default | false | + +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 + +| | | +| ---- | ------------------- | +| Type | string | + +Specify a message describing the changes in this version of the template. Messages longer than 72 characters will be displayed as truncated. + +### --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 + +| | | +| ---- | ------------------------- | +| Type | string-array | + +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 + +| | | +| ---- | ------------------------- | +| Type | string-array | + +Alias of --variable. + +### --variable + +| | | +| ---- | ------------------------- | +| Type | string-array | + +Specify a set of values for Terraform-managed variables. + +### --variables-file + +| | | +| ---- | ------------------- | +| Type | string | + +Specify a file path with values for Terraform-managed variables. + +### -y, --yes + +| | | +| ---- | ----------------- | +| Type | bool | + +Bypass prompts. diff --git a/docs/manifest.json b/docs/manifest.json index abe17edf01593..131f0a03f2896 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -890,6 +890,11 @@ "description": "Archive unused or failed template versions from a given template(s)", "path": "cli/templates_archive.md" }, + { + "title": "templates create", + "description": "Create a template from the current directory or as specified by flag", + "path": "cli/templates_create.md" + }, { "title": "templates delete", "description": "Delete templates", From cb0aec4d4c29b58a1f510404c3275439f6df4a1e Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 4 Jan 2024 19:31:23 +0000 Subject: [PATCH 34/43] fix formatting --- cli/templatecreate.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/templatecreate.go b/cli/templatecreate.go index 87f138514416d..13eaff0b2ffaa 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -46,9 +46,9 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { pretty.Sprint( cliui.DefaultStyles.Warn, "DEPRECATION WARNING: Use `coder templates push` command for creating and updating templates. "+ - "Use `coder templates edit` command for editing template settings."+ - "This command will be removed in a future release."+ - "Waiting 1 second...")+"\n")) + "Use `coder templates edit` command for editing template settings. "+ + "This command will be removed in a future release. \n"+ + "Waiting 1 second...\n"))) time.Sleep(1 * time.Second) isTemplateSchedulingOptionsSet := failureTTL != 0 || dormancyThreshold != 0 || dormancyAutoDeletion != 0 || maxTTL != 0 From d1f13d17b97179a89f616494e2b5570da4a328cd Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 4 Jan 2024 21:28:46 +0000 Subject: [PATCH 35/43] add disableeveryonegroupaccess test --- enterprise/coderd/templates_test.go | 32 +++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index 3c141542fdfc7..62a01f2ee707b 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -808,6 +808,38 @@ func TestTemplateACL(t *testing.T) { require.Equal(t, http.StatusNotFound, cerr.StatusCode()) }) + t.Run("DisableEveryoneGroupAccess", func(t *testing.T) { + t.Parallel() + + client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, + }}) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + acl, err := client.TemplateACL(ctx, template.ID) + require.NoError(t, err) + require.Equal(t, 1, len(acl.Groups)) + _, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{ + Name: template.Name, + DisplayName: template.DisplayName, + Description: template.Description, + Icon: template.Icon, + AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs, + DisableEveryoneGroupAccess: true, + }) + require.NoError(t, err) + + acl, err = client.TemplateACL(ctx, template.ID) + require.NoError(t, err) + require.Equal(t, 0, len(acl.Groups), acl.Groups) + }) + // Test that we do not return deleted users. t.Run("FilterDeletedUsers", func(t *testing.T) { t.Parallel() From 57922796cedaa22ac3592e39f55a4959506c93c2 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 5 Jan 2024 14:59:46 +0000 Subject: [PATCH 36/43] update test --- coderd/database/dbmem/dbmem.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index c533eaaba4589..1ed0d1f73692c 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -6373,6 +6373,7 @@ func (q *FakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.Upd tpl.DisplayName = arg.DisplayName tpl.Description = arg.Description tpl.Icon = arg.Icon + tpl.GroupACL = arg.GroupACL q.templates[idx] = tpl return nil } From 2e54259028467691027328a3e3da3e6266d8ad8c Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 5 Jan 2024 18:49:23 +0000 Subject: [PATCH 37/43] fix test lint --- enterprise/coderd/templates_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index 62a01f2ee707b..b340f90ece7ad 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -811,17 +811,18 @@ func TestTemplateACL(t *testing.T) { t.Run("DisableEveryoneGroupAccess", func(t *testing.T) { t.Parallel() - client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ + client, admin := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ Features: license.Features{ codersdk.FeatureTemplateRBAC: 1, }, }}) - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) + template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() + //nolint:gocritic // non-template-admin cannot get template acl acl, err := client.TemplateACL(ctx, template.ID) require.NoError(t, err) require.Equal(t, 1, len(acl.Groups)) From de78f4bfcfa4cd00b10e8fe3f33693cad9a6b557 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 5 Jan 2024 20:11:51 +0000 Subject: [PATCH 38/43] pr comments --- cli/cliui/deprecation.go | 17 +++ cli/templatecreate.go | 13 +-- enterprise/cli/templatecreate_test.go | 143 ++++++++++++++++++++++++++ 3 files changed, 164 insertions(+), 9 deletions(-) create mode 100644 cli/cliui/deprecation.go create mode 100644 enterprise/cli/templatecreate_test.go diff --git a/cli/cliui/deprecation.go b/cli/cliui/deprecation.go new file mode 100644 index 0000000000000..24c3976ab7ec4 --- /dev/null +++ b/cli/cliui/deprecation.go @@ -0,0 +1,17 @@ +package cliui + +import ( + "github.com/coder/coder/v2/cli/clibase" + "github.com/coder/pretty" +) + +func DeprecationWarning(message string) clibase.MiddlewareFunc { + return func(next clibase.HandlerFunc) clibase.HandlerFunc { + return func(i *clibase.Invocation) error { + pretty.Sprint( + DefaultStyles.Warn, + "DEPRECATION WARNING: This command will be removed in a future release. \n"+message+"\n") + return next(i) + } + } +} diff --git a/cli/templatecreate.go b/cli/templatecreate.go index 13eaff0b2ffaa..b19cfb4b60422 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -39,18 +39,13 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { Short: "Create a template from the current directory or as specified by flag", Middleware: clibase.Chain( clibase.RequireRangeArgs(0, 1), + cliui.DeprecationWarning( + "Use `coder templates push` command for creating and updating templates. "+ + "Use `coder templates edit` command for editing template settings. ", + ), 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: Use `coder templates push` command for creating and updating templates. "+ - "Use `coder templates edit` command for editing template settings. "+ - "This command will be removed in a future release. \n"+ - "Waiting 1 second...\n"))) - time.Sleep(1 * time.Second) - isTemplateSchedulingOptionsSet := failureTTL != 0 || dormancyThreshold != 0 || dormancyAutoDeletion != 0 || maxTTL != 0 if isTemplateSchedulingOptionsSet || requireActiveVersion { diff --git a/enterprise/cli/templatecreate_test.go b/enterprise/cli/templatecreate_test.go new file mode 100644 index 0000000000000..9499810b7df3a --- /dev/null +++ b/enterprise/cli/templatecreate_test.go @@ -0,0 +1,143 @@ +package cli_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/cli/clitest" + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" + "github.com/coder/coder/v2/enterprise/coderd/license" + "github.com/coder/coder/v2/provisioner/echo" + "github.com/coder/coder/v2/testutil" +) + +func TestTemplateCreate(t *testing.T) { + t.Parallel() + + t.Run("RequireActiveVersion", func(t *testing.T) { + t.Parallel() + + client, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureAccessControl: 1, + }, + }, + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }, + }) + templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) + + source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionApply: echo.ApplyComplete, + }) + + inv, conf := newCLI(t, "templates", + "create", "new", + "--directory", source, + "--test.provisioner", string(database.ProvisionerTypeEcho), + "--require-active-version", + "-y", + ) + + clitest.SetupConfig(t, templateAdmin, conf) + + err := inv.Run() + require.NoError(t, err) + + ctx := testutil.Context(t, testutil.WaitMedium) + template, err := templateAdmin.TemplateByName(ctx, user.OrganizationID, "new") + require.NoError(t, err) + require.True(t, template.RequireActiveVersion) + }) + + t.Run("WorkspaceCleanup", func(t *testing.T) { + t.Parallel() + + dv := coderdtest.DeploymentValues(t) + dv.Experiments = []string{ + string(codersdk.ExperimentWorkspaceActions), + } + + client, user := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureAdvancedTemplateScheduling: 1, + }, + }, + Options: &coderdtest.Options{ + DeploymentValues: dv, + IncludeProvisionerDaemon: true, + }, + }) + templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, user.OrganizationID, rbac.RoleTemplateAdmin()) + + source := clitest.CreateTemplateVersionSource(t, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionApply: echo.ApplyComplete, + }) + + const ( + expectedFailureTTL = time.Hour * 3 + expectedDormancyThreshold = time.Hour * 4 + expectedDormancyAutoDeletion = time.Minute * 10 + ) + + inv, conf := newCLI(t, "templates", + "create", "new", + "--directory", source, + "--test.provisioner", string(database.ProvisionerTypeEcho), + "--failure-ttl="+expectedFailureTTL.String(), + "--dormancy-threshold="+expectedDormancyThreshold.String(), + "--dormancy-auto-deletion="+expectedDormancyAutoDeletion.String(), + "-y", + "--", + ) + + clitest.SetupConfig(t, templateAdmin, conf) + + err := inv.Run() + require.NoError(t, err) + + ctx := testutil.Context(t, testutil.WaitMedium) + template, err := templateAdmin.TemplateByName(ctx, user.OrganizationID, "new") + require.NoError(t, err) + require.Equal(t, expectedFailureTTL.Milliseconds(), template.FailureTTLMillis) + require.Equal(t, expectedDormancyThreshold.Milliseconds(), template.TimeTilDormantMillis) + require.Equal(t, expectedDormancyAutoDeletion.Milliseconds(), template.TimeTilDormantAutoDeleteMillis) + }) + + t.Run("NotEntitled", func(t *testing.T) { + t.Parallel() + + client, admin := coderdenttest.New(t, &coderdenttest.Options{ + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{}, + }, + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }, + }) + templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleTemplateAdmin()) + + inv, conf := newCLI(t, "templates", + "create", "new", + "--require-active-version", + "-y", + ) + + clitest.SetupConfig(t, templateAdmin, conf) + + err := inv.Run() + require.Error(t, err) + require.Contains(t, err.Error(), "your license is not entitled to use enterprise access control, so you cannot set --require-active-version") + }) +} From 0c859bc128ee55358439aa9327c0ed25376bad60 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 5 Jan 2024 20:21:21 +0000 Subject: [PATCH 39/43] fix text formatting --- cli/cliui/deprecation.go | 10 +++++++--- cli/templatecreate.go | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cli/cliui/deprecation.go b/cli/cliui/deprecation.go index 24c3976ab7ec4..7673e19fbe11d 100644 --- a/cli/cliui/deprecation.go +++ b/cli/cliui/deprecation.go @@ -1,6 +1,8 @@ package cliui import ( + "fmt" + "github.com/coder/coder/v2/cli/clibase" "github.com/coder/pretty" ) @@ -8,9 +10,11 @@ import ( func DeprecationWarning(message string) clibase.MiddlewareFunc { return func(next clibase.HandlerFunc) clibase.HandlerFunc { return func(i *clibase.Invocation) error { - pretty.Sprint( - DefaultStyles.Warn, - "DEPRECATION WARNING: This command will be removed in a future release. \n"+message+"\n") + _, _ = fmt.Fprintln(i.Stdout, "\n"+pretty.Sprint(DefaultStyles.Wrap, + pretty.Sprint( + DefaultStyles.Warn, + "DEPRECATION WARNING: This command will be removed in a future release."+"\n"+message+"\n"), + )) return next(i) } } diff --git a/cli/templatecreate.go b/cli/templatecreate.go index b19cfb4b60422..4a857c54c4b23 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -40,7 +40,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { Middleware: clibase.Chain( clibase.RequireRangeArgs(0, 1), cliui.DeprecationWarning( - "Use `coder templates push` command for creating and updating templates. "+ + "Use `coder templates push` command for creating and updating templates. \n"+ "Use `coder templates edit` command for editing template settings. ", ), r.InitClient(client), From b65ab816c6963ef8590ee0744910451bbfcf8901 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 5 Jan 2024 20:22:46 +0000 Subject: [PATCH 40/43] Add deprecation to help text --- cli/templatecreate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/templatecreate.go b/cli/templatecreate.go index 4a857c54c4b23..3d52b236fd299 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -36,7 +36,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd { client := new(codersdk.Client) cmd := &clibase.Cmd{ Use: "create [name]", - Short: "Create a template from the current directory or as specified by flag", + Short: "DEPRECATED: Create a template from the current directory or as specified by flag", Middleware: clibase.Chain( clibase.RequireRangeArgs(0, 1), cliui.DeprecationWarning( From 77c9edfdcbff5bd37506710cc2e41ca8545d40fc Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 5 Jan 2024 20:24:01 +0000 Subject: [PATCH 41/43] fix template push wording --- cli/templatepush.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/templatepush.go b/cli/templatepush.go index fe5924e339c9e..26e3aa9472b1a 100644 --- a/cli/templatepush.go +++ b/cli/templatepush.go @@ -38,7 +38,7 @@ func (r *RootCmd) templatePush() *clibase.Cmd { client := new(codersdk.Client) cmd := &clibase.Cmd{ Use: "push [template]", - Short: "Push a new template version from the current directory or as specified by flag", + Short: "Create or update a template from the current directory or as specified by flag", Middleware: clibase.Chain( clibase.RequireRangeArgs(0, 1), r.InitClient(client), From 13794fddce44c62c533e244ab9754bc33bdc1309 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 5 Jan 2024 20:48:00 +0000 Subject: [PATCH 42/43] golden --- cli/testdata/coder_templates_--help.golden | 6 ++--- .../coder_templates_create_--help.golden | 3 ++- .../coder_templates_push_--help.golden | 2 +- docs/cli/templates.md | 22 +++++++++---------- docs/cli/templates_create.md | 2 +- docs/cli/templates_push.md | 2 +- docs/manifest.json | 4 ++-- 7 files changed, 21 insertions(+), 20 deletions(-) diff --git a/cli/testdata/coder_templates_--help.golden b/cli/testdata/coder_templates_--help.golden index 2244a6b787c42..7feaa09e5f429 100644 --- a/cli/testdata/coder_templates_--help.golden +++ b/cli/testdata/coder_templates_--help.golden @@ -21,15 +21,15 @@ USAGE: SUBCOMMANDS: archive Archive unused or failed template versions from a given template(s) - create Create a template from the current directory or as specified by - flag + create DEPRECATED: Create a template from the current directory or as + specified by flag delete Delete templates edit Edit the metadata of a template by name. init Get started with a templated template. list List all the templates available for the organization pull Download the active, latest, or specified version of a template to a path. - push Push a new template version from the current directory or as + push Create or update a template from the current directory or as specified by flag versions Manage different versions of the specified template diff --git a/cli/testdata/coder_templates_create_--help.golden b/cli/testdata/coder_templates_create_--help.golden index ea896d944288b..4fb6512cbab27 100644 --- a/cli/testdata/coder_templates_create_--help.golden +++ b/cli/testdata/coder_templates_create_--help.golden @@ -3,7 +3,8 @@ coder v0.0.0-devel USAGE: coder templates create [flags] [name] - Create a template from the current directory or as specified by flag + DEPRECATED: Create a template from the current directory or as specified by + flag OPTIONS: --default-ttl duration (default: 24h) diff --git a/cli/testdata/coder_templates_push_--help.golden b/cli/testdata/coder_templates_push_--help.golden index 4e17e00caca47..092e16f897bee 100644 --- a/cli/testdata/coder_templates_push_--help.golden +++ b/cli/testdata/coder_templates_push_--help.golden @@ -3,7 +3,7 @@ coder v0.0.0-devel USAGE: coder templates push [flags] [template] - Push a new template version from the current directory or as specified by flag + Create or update a template from the current directory or as specified by flag OPTIONS: --activate bool (default: true) diff --git a/docs/cli/templates.md b/docs/cli/templates.md index 8a39938296584..0226bd5a60d92 100644 --- a/docs/cli/templates.md +++ b/docs/cli/templates.md @@ -30,14 +30,14 @@ workspaces: ## Subcommands -| Name | Purpose | -| ------------------------------------------------ | ------------------------------------------------------------------------------ | -| [archive](./templates_archive.md) | Archive unused or failed template versions from a given template(s) | -| [create](./templates_create.md) | Create a template from the current directory or as specified by flag | -| [delete](./templates_delete.md) | Delete templates | -| [edit](./templates_edit.md) | Edit the metadata of a template by name. | -| [init](./templates_init.md) | Get started with a templated template. | -| [list](./templates_list.md) | List all the templates available for the organization | -| [pull](./templates_pull.md) | Download the active, latest, or specified version of a template to a path. | -| [push](./templates_push.md) | Push a new template version from the current directory or as specified by flag | -| [versions](./templates_versions.md) | Manage different versions of the specified template | +| Name | Purpose | +| ------------------------------------------------ | -------------------------------------------------------------------------------- | +| [archive](./templates_archive.md) | Archive unused or failed template versions from a given template(s) | +| [create](./templates_create.md) | DEPRECATED: Create a template from the current directory or as specified by flag | +| [delete](./templates_delete.md) | Delete templates | +| [edit](./templates_edit.md) | Edit the metadata of a template by name. | +| [init](./templates_init.md) | Get started with a templated template. | +| [list](./templates_list.md) | List all the templates available for the organization | +| [pull](./templates_pull.md) | Download the active, latest, or specified version of a template to a path. | +| [push](./templates_push.md) | Create or update a template from the current directory or as specified by flag | +| [versions](./templates_versions.md) | Manage different versions of the specified template | diff --git a/docs/cli/templates_create.md b/docs/cli/templates_create.md index 9535e2f12e6da..eacac108501db 100644 --- a/docs/cli/templates_create.md +++ b/docs/cli/templates_create.md @@ -2,7 +2,7 @@ # templates create -Create a template from the current directory or as specified by flag +DEPRECATED: Create a template from the current directory or as specified by flag ## Usage diff --git a/docs/cli/templates_push.md b/docs/cli/templates_push.md index bac0b971cd76c..d7a6cb7043989 100644 --- a/docs/cli/templates_push.md +++ b/docs/cli/templates_push.md @@ -2,7 +2,7 @@ # templates push -Push a new template version from the current directory or as specified by flag +Create or update a template from the current directory or as specified by flag ## Usage diff --git a/docs/manifest.json b/docs/manifest.json index 131f0a03f2896..bef7dc89f5511 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -892,7 +892,7 @@ }, { "title": "templates create", - "description": "Create a template from the current directory or as specified by flag", + "description": "DEPRECATED: Create a template from the current directory or as specified by flag", "path": "cli/templates_create.md" }, { @@ -922,7 +922,7 @@ }, { "title": "templates push", - "description": "Push a new template version from the current directory or as specified by flag", + "description": "Create or update a template from the current directory or as specified by flag", "path": "cli/templates_push.md" }, { From 8355850080fe29e4fd12fd8d8ec66a19d0feb499 Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Fri, 5 Jan 2024 20:54:18 +0000 Subject: [PATCH 43/43] fix gen again --- site/src/api/typesGenerated.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 4bbea6cc062cd..b38c1b48298eb 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -809,6 +809,7 @@ export interface ProvisionerDaemon { readonly last_seen_at?: string; readonly name: string; readonly version: string; + readonly api_version: string; readonly provisioners: ProvisionerType[]; readonly tags: Record; }