Skip to content

Commit f5f150d

Browse files
authored
feat: add cli support for --require-active-version (#10337)
1 parent b799014 commit f5f150d

19 files changed

+626
-50
lines changed

cli/restart.go

+23-12
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ package cli
22

33
import (
44
"fmt"
5+
"net/http"
56
"time"
67

78
"golang.org/x/xerrors"
89

9-
"github.com/coder/pretty"
10-
1110
"github.com/coder/coder/v2/cli/clibase"
1211
"github.com/coder/coder/v2/cli/cliui"
1312
"github.com/coder/coder/v2/codersdk"
13+
"github.com/coder/pretty"
1414
)
1515

1616
func (r *RootCmd) restart() *clibase.Cmd {
@@ -40,19 +40,14 @@ func (r *RootCmd) restart() *clibase.Cmd {
4040
return err
4141
}
4242

43-
template, err := client.Template(inv.Context(), workspace.TemplateID)
44-
if err != nil {
45-
return err
46-
}
47-
4843
buildOptions, err := asWorkspaceBuildParameters(parameterFlags.buildOptions)
4944
if err != nil {
5045
return xerrors.Errorf("can't parse build options: %w", err)
5146
}
5247

5348
buildParameters, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{
54-
Action: WorkspaceRestart,
55-
Template: template,
49+
Action: WorkspaceRestart,
50+
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
5651

5752
LastBuildParameters: lastBuildParameters,
5853

@@ -82,13 +77,29 @@ func (r *RootCmd) restart() *clibase.Cmd {
8277
return err
8378
}
8479

85-
build, err = client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
80+
req := codersdk.CreateWorkspaceBuildRequest{
8681
Transition: codersdk.WorkspaceTransitionStart,
8782
RichParameterValues: buildParameters,
88-
})
89-
if err != nil {
83+
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
84+
}
85+
86+
build, err = client.CreateWorkspaceBuild(ctx, workspace.ID, req)
87+
// It's possible for a workspace build to fail due to the template requiring starting
88+
// workspaces with the active version.
89+
if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusUnauthorized {
90+
build, err = startWorkspaceActiveVersion(inv, client, startWorkspaceActiveVersionArgs{
91+
BuildOptions: buildOptions,
92+
LastBuildParameters: lastBuildParameters,
93+
PromptBuildOptions: parameterFlags.promptBuildOptions,
94+
Workspace: workspace,
95+
})
96+
if err != nil {
97+
return xerrors.Errorf("start workspace with active template version: %w", err)
98+
}
99+
} else if err != nil {
90100
return err
91101
}
102+
92103
err = cliui.WorkspaceBuild(ctx, out, client, build.ID)
93104
if err != nil {
94105
return err

cli/start.go

+65-13
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package cli
22

33
import (
44
"fmt"
5+
"net/http"
56
"time"
67

8+
"github.com/google/uuid"
79
"golang.org/x/xerrors"
810

911
"github.com/coder/coder/v2/cli/clibase"
@@ -35,19 +37,14 @@ func (r *RootCmd) start() *clibase.Cmd {
3537
return err
3638
}
3739

38-
template, err := client.Template(inv.Context(), workspace.TemplateID)
39-
if err != nil {
40-
return err
41-
}
42-
4340
buildOptions, err := asWorkspaceBuildParameters(parameterFlags.buildOptions)
4441
if err != nil {
4542
return xerrors.Errorf("unable to parse build options: %w", err)
4643
}
4744

4845
buildParameters, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{
49-
Action: WorkspaceStart,
50-
Template: template,
46+
Action: WorkspaceStart,
47+
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
5148

5249
LastBuildParameters: lastBuildParameters,
5350

@@ -58,11 +55,26 @@ func (r *RootCmd) start() *clibase.Cmd {
5855
return err
5956
}
6057

61-
build, err := client.CreateWorkspaceBuild(inv.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
58+
req := codersdk.CreateWorkspaceBuildRequest{
6259
Transition: codersdk.WorkspaceTransitionStart,
6360
RichParameterValues: buildParameters,
64-
})
65-
if err != nil {
61+
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
62+
}
63+
64+
build, err := client.CreateWorkspaceBuild(inv.Context(), workspace.ID, req)
65+
// It's possible for a workspace build to fail due to the template requiring starting
66+
// workspaces with the active version.
67+
if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusUnauthorized {
68+
build, err = startWorkspaceActiveVersion(inv, client, startWorkspaceActiveVersionArgs{
69+
BuildOptions: buildOptions,
70+
LastBuildParameters: lastBuildParameters,
71+
PromptBuildOptions: parameterFlags.promptBuildOptions,
72+
Workspace: workspace,
73+
})
74+
if err != nil {
75+
return xerrors.Errorf("start workspace with active template version: %w", err)
76+
}
77+
} else if err != nil {
6678
return err
6779
}
6880

@@ -82,8 +94,8 @@ func (r *RootCmd) start() *clibase.Cmd {
8294
}
8395

8496
type prepStartWorkspaceArgs struct {
85-
Action WorkspaceCLIAction
86-
Template codersdk.Template
97+
Action WorkspaceCLIAction
98+
TemplateVersionID uuid.UUID
8799

88100
LastBuildParameters []codersdk.WorkspaceBuildParameter
89101

@@ -94,7 +106,7 @@ type prepStartWorkspaceArgs struct {
94106
func prepStartWorkspace(inv *clibase.Invocation, client *codersdk.Client, args prepStartWorkspaceArgs) ([]codersdk.WorkspaceBuildParameter, error) {
95107
ctx := inv.Context()
96108

97-
templateVersion, err := client.TemplateVersion(ctx, args.Template.ActiveVersionID)
109+
templateVersion, err := client.TemplateVersion(ctx, args.TemplateVersionID)
98110
if err != nil {
99111
return nil, xerrors.Errorf("get template version: %w", err)
100112
}
@@ -110,3 +122,43 @@ func prepStartWorkspace(inv *clibase.Invocation, client *codersdk.Client, args p
110122
WithBuildOptions(args.BuildOptions)
111123
return resolver.Resolve(inv, args.Action, templateVersionParameters)
112124
}
125+
126+
type startWorkspaceActiveVersionArgs struct {
127+
BuildOptions []codersdk.WorkspaceBuildParameter
128+
LastBuildParameters []codersdk.WorkspaceBuildParameter
129+
PromptBuildOptions bool
130+
Workspace codersdk.Workspace
131+
}
132+
133+
func startWorkspaceActiveVersion(inv *clibase.Invocation, client *codersdk.Client, args startWorkspaceActiveVersionArgs) (codersdk.WorkspaceBuild, error) {
134+
_, _ = fmt.Fprintln(inv.Stdout, "Failed to restart with the template version from your last build. Policy may require you to restart with the current active template version.")
135+
136+
template, err := client.Template(inv.Context(), args.Workspace.TemplateID)
137+
if err != nil {
138+
return codersdk.WorkspaceBuild{}, xerrors.Errorf("get template: %w", err)
139+
}
140+
141+
buildParameters, err := prepStartWorkspace(inv, client, prepStartWorkspaceArgs{
142+
Action: WorkspaceStart,
143+
TemplateVersionID: template.ActiveVersionID,
144+
145+
LastBuildParameters: args.LastBuildParameters,
146+
147+
PromptBuildOptions: args.PromptBuildOptions,
148+
BuildOptions: args.BuildOptions,
149+
})
150+
if err != nil {
151+
return codersdk.WorkspaceBuild{}, err
152+
}
153+
154+
build, err := client.CreateWorkspaceBuild(inv.Context(), args.Workspace.ID, codersdk.CreateWorkspaceBuildRequest{
155+
Transition: codersdk.WorkspaceTransitionStart,
156+
RichParameterValues: buildParameters,
157+
TemplateVersionID: template.ActiveVersionID,
158+
})
159+
if err != nil {
160+
return codersdk.WorkspaceBuild{}, err
161+
}
162+
163+
return build, nil
164+
}

cli/templatecreate.go

+38-11
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ import (
2424

2525
func (r *RootCmd) templateCreate() *clibase.Cmd {
2626
var (
27-
provisioner string
28-
provisionerTags []string
29-
variablesFile string
30-
variables []string
31-
disableEveryone bool
27+
provisioner string
28+
provisionerTags []string
29+
variablesFile string
30+
variables []string
31+
disableEveryone bool
32+
requireActiveVersion bool
3233

3334
defaultTTL time.Duration
3435
failureTTL time.Duration
@@ -46,17 +47,35 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
4647
r.InitClient(client),
4748
),
4849
Handler: func(inv *clibase.Invocation) error {
49-
if failureTTL != 0 || inactivityTTL != 0 || maxTTL != 0 {
50+
isTemplateSchedulingOptionsSet := failureTTL != 0 || inactivityTTL != 0 || maxTTL != 0
51+
52+
if isTemplateSchedulingOptionsSet || requireActiveVersion {
5053
entitlements, err := client.Entitlements(inv.Context())
51-
var sdkErr *codersdk.Error
52-
if xerrors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusNotFound {
53-
return xerrors.Errorf("your deployment appears to be an AGPL deployment, so you cannot set --failure-ttl or --inactivityTTL")
54+
if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusNotFound {
55+
return xerrors.Errorf("your deployment appears to be an AGPL deployment, so you cannot set enterprise-only flags")
5456
} else if err != nil {
5557
return xerrors.Errorf("get entitlements: %w", err)
5658
}
5759

58-
if !entitlements.Features[codersdk.FeatureAdvancedTemplateScheduling].Enabled {
59-
return xerrors.Errorf("your license is not entitled to use advanced template scheduling, so you cannot set --failure-ttl or --inactivityTTL")
60+
if isTemplateSchedulingOptionsSet {
61+
if !entitlements.Features[codersdk.FeatureAdvancedTemplateScheduling].Enabled {
62+
return xerrors.Errorf("your license is not entitled to use advanced template scheduling, so you cannot set --failure-ttl or --inactivityTTL")
63+
}
64+
}
65+
66+
if requireActiveVersion {
67+
if !entitlements.Features[codersdk.FeatureAccessControl].Enabled {
68+
return xerrors.Errorf("your license is not entitled to use enterprise access control, so you cannot set --require-active-version")
69+
}
70+
71+
experiments, exErr := client.Experiments(inv.Context())
72+
if exErr != nil {
73+
return xerrors.Errorf("get experiments: %w", exErr)
74+
}
75+
76+
if !experiments.Enabled(codersdk.ExperimentTemplateUpdatePolicies) {
77+
return xerrors.Errorf("--require-active-version is an experimental feature, contact an administrator to enable the 'template_update_policies' experiment on your Coder server")
78+
}
6079
}
6180
}
6281

@@ -129,6 +148,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
129148
MaxTTLMillis: ptr.Ref(maxTTL.Milliseconds()),
130149
TimeTilDormantMillis: ptr.Ref(inactivityTTL.Milliseconds()),
131150
DisableEveryoneGroupAccess: disableEveryone,
151+
RequireActiveVersion: requireActiveVersion,
132152
}
133153

134154
_, err = client.CreateTemplate(inv.Context(), organization.ID, createReq)
@@ -205,6 +225,13 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
205225
Value: clibase.StringOf(&provisioner),
206226
Hidden: true,
207227
},
228+
{
229+
Flag: "require-active-version",
230+
Description: "Requires workspace builds to use the active template version. This setting does not apply to template admins. This is an enterprise-only feature.",
231+
Value: clibase.BoolOf(&requireActiveVersion),
232+
Default: "false",
233+
},
234+
208235
cliui.SkipPromptOption(),
209236
}
210237
cmd.Options = append(cmd.Options, uploadFlags.options()...)

cli/templatecreate_test.go

+31
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/coder/coder/v2/cli/clitest"
1414
"github.com/coder/coder/v2/coderd/coderdtest"
1515
"github.com/coder/coder/v2/coderd/database"
16+
"github.com/coder/coder/v2/codersdk"
1617
"github.com/coder/coder/v2/provisioner/echo"
1718
"github.com/coder/coder/v2/provisionersdk/proto"
1819
"github.com/coder/coder/v2/pty/ptytest"
@@ -393,6 +394,36 @@ func TestTemplateCreate(t *testing.T) {
393394
}
394395
}
395396
})
397+
398+
t.Run("RequireActiveVersionInvalid", func(t *testing.T) {
399+
t.Parallel()
400+
401+
dv := coderdtest.DeploymentValues(t)
402+
dv.Experiments = []string{
403+
string(codersdk.ExperimentTemplateUpdatePolicies),
404+
}
405+
406+
client := coderdtest.New(t, &coderdtest.Options{
407+
IncludeProvisionerDaemon: true,
408+
DeploymentValues: dv,
409+
})
410+
coderdtest.CreateFirstUser(t, client)
411+
source := clitest.CreateTemplateVersionSource(t, completeWithAgent())
412+
args := []string{
413+
"templates",
414+
"create",
415+
"my-template",
416+
"--directory", source,
417+
"--test.provisioner", string(database.ProvisionerTypeEcho),
418+
"--require-active-version",
419+
}
420+
inv, root := clitest.New(t, args...)
421+
clitest.SetupConfig(t, client, root)
422+
423+
err := inv.Run()
424+
require.Error(t, err)
425+
require.Contains(t, err.Error(), "your deployment appears to be an AGPL deployment, so you cannot set enterprise-only flags")
426+
})
396427
}
397428

398429
// Need this for Windows because of a known issue with Go:

0 commit comments

Comments
 (0)