Skip to content

Commit 7b0afe8

Browse files
committed
fix: make template push a superset of template create
1 parent 45e9d93 commit 7b0afe8

File tree

2 files changed

+179
-57
lines changed

2 files changed

+179
-57
lines changed

cli/templatecreate.go

Lines changed: 70 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cli
22

33
import (
4+
"context"
45
"errors"
56
"fmt"
67
"io"
@@ -46,48 +47,17 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
4647
r.InitClient(client),
4748
),
4849
Handler: func(inv *clibase.Invocation) error {
49-
isTemplateSchedulingOptionsSet := failureTTL != 0 || dormancyThreshold != 0 || dormancyAutoDeletion != 0 || maxTTL != 0
50-
51-
if isTemplateSchedulingOptionsSet || requireActiveVersion {
52-
if failureTTL != 0 || dormancyThreshold != 0 || dormancyAutoDeletion != 0 {
53-
// This call can be removed when workspace_actions is no longer experimental
54-
experiments, exErr := client.Experiments(inv.Context())
55-
if exErr != nil {
56-
return xerrors.Errorf("get experiments: %w", exErr)
57-
}
58-
59-
if !experiments.Enabled(codersdk.ExperimentWorkspaceActions) {
60-
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.")
61-
}
62-
}
63-
64-
entitlements, err := client.Entitlements(inv.Context())
65-
if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusNotFound {
66-
return xerrors.Errorf("your deployment appears to be an AGPL deployment, so you cannot set enterprise-only flags")
67-
} else if err != nil {
68-
return xerrors.Errorf("get entitlements: %w", err)
69-
}
70-
71-
if isTemplateSchedulingOptionsSet {
72-
if !entitlements.Features[codersdk.FeatureAdvancedTemplateScheduling].Enabled {
73-
return xerrors.Errorf("your license is not entitled to use advanced template scheduling, so you cannot set --failure-ttl, --inactivity-ttl, or --max-ttl")
74-
}
75-
}
76-
77-
if requireActiveVersion {
78-
if !entitlements.Features[codersdk.FeatureAccessControl].Enabled {
79-
return xerrors.Errorf("your license is not entitled to use enterprise access control, so you cannot set --require-active-version")
80-
}
81-
82-
experiments, exErr := client.Experiments(inv.Context())
83-
if exErr != nil {
84-
return xerrors.Errorf("get experiments: %w", exErr)
85-
}
86-
87-
if !experiments.Enabled(codersdk.ExperimentTemplateUpdatePolicies) {
88-
return xerrors.Errorf("--require-active-version is an experimental feature, contact an administrator to enable the 'template_update_policies' experiment on your Coder server")
89-
}
90-
}
50+
err := handleEntitlements(inv.Context(), handleEntitlementsArgs{
51+
client: client,
52+
requireActiveVersion: requireActiveVersion,
53+
defaultTTL: defaultTTL,
54+
failureTTL: failureTTL,
55+
dormancyThreshold: dormancyThreshold,
56+
dormancyAutoDeletion: dormancyAutoDeletion,
57+
maxTTL: maxTTL,
58+
})
59+
if err != nil {
60+
return err
9161
}
9262

9363
organization, err := CurrentOrganization(inv, client)
@@ -366,3 +336,61 @@ func ParseProvisionerTags(rawTags []string) (map[string]string, error) {
366336
}
367337
return tags, nil
368338
}
339+
340+
type handleEntitlementsArgs struct {
341+
client *codersdk.Client
342+
requireActiveVersion bool
343+
defaultTTL time.Duration
344+
failureTTL time.Duration
345+
dormancyThreshold time.Duration
346+
dormancyAutoDeletion time.Duration
347+
maxTTL time.Duration
348+
}
349+
350+
func handleEntitlements(ctx context.Context, args handleEntitlementsArgs) error {
351+
isTemplateSchedulingOptionsSet := args.failureTTL != 0 || args.dormancyThreshold != 0 || args.dormancyAutoDeletion != 0 || args.maxTTL != 0
352+
353+
if isTemplateSchedulingOptionsSet || args.requireActiveVersion {
354+
if args.failureTTL != 0 || args.dormancyThreshold != 0 || args.dormancyAutoDeletion != 0 {
355+
// This call can be removed when workspace_actions is no longer experimental
356+
experiments, exErr := args.client.Experiments(ctx)
357+
if exErr != nil {
358+
return xerrors.Errorf("get experiments: %w", exErr)
359+
}
360+
361+
if !experiments.Enabled(codersdk.ExperimentWorkspaceActions) {
362+
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.")
363+
}
364+
}
365+
366+
entitlements, err := args.client.Entitlements(ctx)
367+
if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusNotFound {
368+
return xerrors.Errorf("your deployment appears to be an AGPL deployment, so you cannot set enterprise-only flags")
369+
} else if err != nil {
370+
return xerrors.Errorf("get entitlements: %w", err)
371+
}
372+
373+
if isTemplateSchedulingOptionsSet {
374+
if !entitlements.Features[codersdk.FeatureAdvancedTemplateScheduling].Enabled {
375+
return xerrors.Errorf("your license is not entitled to use advanced template scheduling, so you cannot set --failure-ttl, --inactivity-ttl, or --max-ttl")
376+
}
377+
}
378+
379+
if args.requireActiveVersion {
380+
if !entitlements.Features[codersdk.FeatureAccessControl].Enabled {
381+
return xerrors.Errorf("your license is not entitled to use enterprise access control, so you cannot set --require-active-version")
382+
}
383+
384+
experiments, exErr := args.client.Experiments(ctx)
385+
if exErr != nil {
386+
return xerrors.Errorf("get experiments: %w", exErr)
387+
}
388+
389+
if !experiments.Enabled(codersdk.ExperimentTemplateUpdatePolicies) {
390+
return xerrors.Errorf("--require-active-version is an experimental feature, contact an administrator to enable the 'template_update_policies' experiment on your Coder server")
391+
}
392+
}
393+
}
394+
395+
return nil
396+
}

cli/templatepush.go

Lines changed: 109 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ package cli
22

33
import (
44
"bufio"
5+
"errors"
56
"fmt"
67
"io"
8+
"net/http"
79
"os"
810
"path/filepath"
911
"strings"
1012
"time"
13+
"unicode/utf8"
1114

1215
"github.com/briandowns/spinner"
1316
"golang.org/x/xerrors"
@@ -158,14 +161,20 @@ func (r *RootCmd) templatePush() *clibase.Cmd {
158161
var (
159162
versionName string
160163
provisioner string
161-
workdir string
162164
variablesFile string
163165
commandLineVariables []string
164166
alwaysPrompt bool
165167
provisionerTags []string
166168
uploadFlags templateUploadFlags
167169
activate bool
168-
create bool
170+
171+
requireActiveVersion bool
172+
disableEveryone bool
173+
defaultTTL time.Duration
174+
failureTTL time.Duration
175+
dormancyThreshold time.Duration
176+
dormancyAutoDeletion time.Duration
177+
maxTTL time.Duration
169178
)
170179
client := new(codersdk.Client)
171180
cmd := &clibase.Cmd{
@@ -176,7 +185,18 @@ func (r *RootCmd) templatePush() *clibase.Cmd {
176185
r.InitClient(client),
177186
),
178187
Handler: func(inv *clibase.Invocation) error {
179-
uploadFlags.setWorkdir(workdir)
188+
err := handleEntitlements(inv.Context(), handleEntitlementsArgs{
189+
client: client,
190+
requireActiveVersion: requireActiveVersion,
191+
defaultTTL: defaultTTL,
192+
failureTTL: failureTTL,
193+
dormancyThreshold: dormancyThreshold,
194+
dormancyAutoDeletion: dormancyAutoDeletion,
195+
maxTTL: maxTTL,
196+
})
197+
if err != nil {
198+
return err
199+
}
180200

181201
organization, err := CurrentOrganization(inv, client)
182202
if err != nil {
@@ -188,10 +208,15 @@ func (r *RootCmd) templatePush() *clibase.Cmd {
188208
return err
189209
}
190210

211+
if utf8.RuneCountInString(name) > 31 {
212+
return xerrors.Errorf("Template name must be less than 32 characters")
213+
}
214+
191215
var createTemplate bool
192216
template, err := client.TemplateByName(inv.Context(), organization.ID, name)
193217
if err != nil {
194-
if !create {
218+
var apiError *codersdk.Error
219+
if errors.As(err, &apiError) && apiError.StatusCode() != http.StatusNotFound {
195220
return err
196221
}
197222
createTemplate = true
@@ -268,6 +293,48 @@ func (r *RootCmd) templatePush() *clibase.Cmd {
268293
}
269294
}
270295

296+
editTemplate := requireActiveVersion ||
297+
disableEveryone ||
298+
defaultTTL != 0 ||
299+
failureTTL != 0 ||
300+
dormancyThreshold != 0 ||
301+
dormancyAutoDeletion != 0 ||
302+
maxTTL != 0
303+
if editTemplate {
304+
if defaultTTL == 0 {
305+
defaultTTL = time.Duration(template.DefaultTTLMillis) * time.Millisecond
306+
}
307+
if failureTTL == 0 {
308+
failureTTL = time.Duration(template.FailureTTLMillis) * time.Millisecond
309+
}
310+
if dormancyThreshold == 0 {
311+
dormancyThreshold = time.Duration(template.TimeTilDormantMillis) * time.Millisecond
312+
}
313+
if dormancyAutoDeletion == 0 {
314+
dormancyAutoDeletion = time.Duration(template.TimeTilDormantAutoDeleteMillis) * time.Millisecond
315+
}
316+
if maxTTL == 0 {
317+
maxTTL = time.Duration(template.MaxTTLMillis) * time.Millisecond
318+
}
319+
req := codersdk.UpdateTemplateMeta{
320+
RequireActiveVersion: requireActiveVersion,
321+
DisableEveryone: disableEveryone,
322+
DefaultTTLMillis: defaultTTL.Milliseconds(),
323+
FailureTTLMillis: failureTTL.Milliseconds(),
324+
TimeTilDormantMillis: dormancyThreshold.Milliseconds(),
325+
TimeTilDormantAutoDeleteMillis: dormancyAutoDeletion.Milliseconds(),
326+
MaxTTLMillis: maxTTL.Milliseconds(),
327+
}
328+
329+
_, err = client.UpdateTemplateMeta(inv.Context(), template.ID, req)
330+
if err != nil {
331+
return xerrors.Errorf("update template metadata: %w", err)
332+
}
333+
if err != nil {
334+
return err
335+
}
336+
}
337+
271338
_, _ = fmt.Fprintf(inv.Stdout, "Updated version at %s!\n", pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp)))
272339
return nil
273340
},
@@ -282,14 +349,6 @@ func (r *RootCmd) templatePush() *clibase.Cmd {
282349
// This is for testing!
283350
Hidden: true,
284351
},
285-
{
286-
Flag: "test.workdir",
287-
Description: "Customize the working directory.",
288-
Default: "",
289-
Value: clibase.StringOf(&workdir),
290-
// This is for testing!
291-
Hidden: true,
292-
},
293352
{
294353
Flag: "variables-file",
295354
Description: "Specify a file path with values for Terraform-managed variables.",
@@ -327,10 +386,45 @@ func (r *RootCmd) templatePush() *clibase.Cmd {
327386
Value: clibase.BoolOf(&activate),
328387
},
329388
{
330-
Flag: "create",
331-
Description: "Create the template if it does not exist.",
389+
Flag: "require-active-version",
390+
Description: "Requires workspace builds to use the active template version. This setting does not apply to template admins. This is an enterprise-only feature.",
391+
Value: clibase.BoolOf(&requireActiveVersion),
332392
Default: "false",
333-
Value: clibase.BoolOf(&create),
393+
},
394+
{
395+
Flag: "default-ttl",
396+
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.",
397+
Default: "24h",
398+
Value: clibase.DurationOf(&defaultTTL),
399+
},
400+
{
401+
Flag: "failure-ttl",
402+
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.",
403+
Default: "0h",
404+
Value: clibase.DurationOf(&failureTTL),
405+
},
406+
{
407+
Flag: "dormancy-threshold",
408+
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.",
409+
Default: "0h",
410+
Value: clibase.DurationOf(&dormancyThreshold),
411+
},
412+
{
413+
Flag: "dormancy-auto-deletion",
414+
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.",
415+
Default: "0h",
416+
Value: clibase.DurationOf(&dormancyAutoDeletion),
417+
},
418+
{
419+
Flag: "max-ttl",
420+
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.",
421+
Value: clibase.DurationOf(&maxTTL),
422+
},
423+
{
424+
Flag: "private",
425+
Description: "Disable the default behavior of granting template access to the 'everyone' group. " +
426+
"The template permissions must be updated to allow non-admin users to use this template.",
427+
Value: clibase.BoolOf(&disableEveryone),
334428
},
335429
cliui.SkipPromptOption(),
336430
}

0 commit comments

Comments
 (0)