Skip to content

Commit e33941b

Browse files
authored
feat: allow disabling autostart and custom autostop for template (#6933)
API only, frontend in upcoming PR.
1 parent 083fc89 commit e33941b

File tree

65 files changed

+1432
-485
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+1432
-485
lines changed

cli/server.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"strconv"
3131
"strings"
3232
"sync"
33+
"sync/atomic"
3334
"time"
3435

3536
"github.com/coreos/go-oidc/v3/oidc"
@@ -72,6 +73,7 @@ import (
7273
"github.com/coder/coder/coderd/httpapi"
7374
"github.com/coder/coder/coderd/httpmw"
7475
"github.com/coder/coder/coderd/prometheusmetrics"
76+
"github.com/coder/coder/coderd/schedule"
7577
"github.com/coder/coder/coderd/telemetry"
7678
"github.com/coder/coder/coderd/tracing"
7779
"github.com/coder/coder/coderd/updatecheck"
@@ -632,6 +634,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
632634
LoginRateLimit: loginRateLimit,
633635
FilesRateLimit: filesRateLimit,
634636
HTTPClient: httpClient,
637+
TemplateScheduleStore: &atomic.Pointer[schedule.TemplateScheduleStore]{},
635638
SSHConfig: codersdk.SSHConfigResponse{
636639
HostnamePrefix: cfg.SSHConfig.DeploymentName.String(),
637640
SSHConfigOptions: configSSHOptions,
@@ -1019,7 +1022,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
10191022

10201023
autobuildPoller := time.NewTicker(cfg.AutobuildPollInterval.Value())
10211024
defer autobuildPoller.Stop()
1022-
autobuildExecutor := executor.New(ctx, options.Database, logger, autobuildPoller.C)
1025+
autobuildExecutor := executor.New(ctx, options.Database, coderAPI.TemplateScheduleStore, logger, autobuildPoller.C)
10231026
autobuildExecutor.Run()
10241027

10251028
// Currently there is no way to ask the server to shut

cli/templateedit.go

+19-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
2121
defaultTTL time.Duration
2222
maxTTL time.Duration
2323
allowUserCancelWorkspaceJobs bool
24+
allowUserAutostart bool
25+
allowUserAutostop bool
2426
)
2527
client := new(codersdk.Client)
2628

@@ -32,17 +34,17 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
3234
),
3335
Short: "Edit the metadata of a template by name.",
3436
Handler: func(inv *clibase.Invocation) error {
35-
if maxTTL != 0 {
37+
if maxTTL != 0 || !allowUserAutostart || !allowUserAutostop {
3638
entitlements, err := client.Entitlements(inv.Context())
3739
var sdkErr *codersdk.Error
3840
if xerrors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusNotFound {
39-
return xerrors.Errorf("your deployment appears to be an AGPL deployment, so you cannot set --max-ttl")
41+
return xerrors.Errorf("your deployment appears to be an AGPL deployment, so you cannot set --max-ttl, --allow-user-autostart=false or --allow-user-autostop=false")
4042
} else if err != nil {
4143
return xerrors.Errorf("get entitlements: %w", err)
4244
}
4345

4446
if !entitlements.Features[codersdk.FeatureAdvancedTemplateScheduling].Enabled {
45-
return xerrors.Errorf("your license is not entitled to use advanced template scheduling, so you cannot set --max-ttl")
47+
return xerrors.Errorf("your license is not entitled to use advanced template scheduling, so you cannot set --max-ttl, --allow-user-autostart=false or --allow-user-autostop=false")
4648
}
4749
}
4850

@@ -64,6 +66,8 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
6466
DefaultTTLMillis: defaultTTL.Milliseconds(),
6567
MaxTTLMillis: maxTTL.Milliseconds(),
6668
AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs,
69+
AllowUserAutostart: allowUserAutostart,
70+
AllowUserAutostop: allowUserAutostop,
6771
}
6872

6973
_, err = client.UpdateTemplateMeta(inv.Context(), template.ID, req)
@@ -112,6 +116,18 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
112116
Default: "true",
113117
Value: clibase.BoolOf(&allowUserCancelWorkspaceJobs),
114118
},
119+
{
120+
Flag: "allow-user-autostart",
121+
Description: "Allow users to configure autostart for workspaces on this template. This can only be disabled in enterprise.",
122+
Default: "true",
123+
Value: clibase.BoolOf(&allowUserAutostart),
124+
},
125+
{
126+
Flag: "allow-user-autostop",
127+
Description: "Allow users to customize the autostop TTL for workspaces on this template. This can only be disabled in enterprise.",
128+
Default: "true",
129+
Value: clibase.BoolOf(&allowUserAutostop),
130+
},
115131
cliui.SkipPromptOption(),
116132
}
117133

cli/templateedit_test.go

+233
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,147 @@ func TestTemplateEdit(t *testing.T) {
428428

429429
require.EqualValues(t, 1, atomic.LoadInt64(&updateTemplateCalled))
430430

431+
// Assert that the template metadata did not change. We verify the
432+
// correct request gets sent to the server already.
433+
updated, err := client.Template(context.Background(), template.ID)
434+
require.NoError(t, err)
435+
assert.Equal(t, template.Name, updated.Name)
436+
assert.Equal(t, template.Description, updated.Description)
437+
assert.Equal(t, template.Icon, updated.Icon)
438+
assert.Equal(t, template.DisplayName, updated.DisplayName)
439+
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
440+
assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
441+
})
442+
})
443+
t.Run("AllowUserScheduling", func(t *testing.T) {
444+
t.Parallel()
445+
t.Run("BlockedAGPL", func(t *testing.T) {
446+
t.Parallel()
447+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
448+
user := coderdtest.CreateFirstUser(t, client)
449+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
450+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
451+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
452+
ctr.DefaultTTLMillis = nil
453+
ctr.MaxTTLMillis = nil
454+
})
455+
456+
// Test the cli command with --allow-user-autostart.
457+
cmdArgs := []string{
458+
"templates",
459+
"edit",
460+
template.Name,
461+
"--allow-user-autostart=false",
462+
}
463+
inv, root := clitest.New(t, cmdArgs...)
464+
clitest.SetupConfig(t, client, root)
465+
466+
ctx := testutil.Context(t, testutil.WaitLong)
467+
err := inv.WithContext(ctx).Run()
468+
require.Error(t, err)
469+
require.ErrorContains(t, err, "appears to be an AGPL deployment")
470+
471+
// Test the cli command with --allow-user-autostop.
472+
cmdArgs = []string{
473+
"templates",
474+
"edit",
475+
template.Name,
476+
"--allow-user-autostop=false",
477+
}
478+
inv, root = clitest.New(t, cmdArgs...)
479+
clitest.SetupConfig(t, client, root)
480+
481+
ctx = testutil.Context(t, testutil.WaitLong)
482+
err = inv.WithContext(ctx).Run()
483+
require.Error(t, err)
484+
require.ErrorContains(t, err, "appears to be an AGPL deployment")
485+
486+
// Assert that the template metadata did not change.
487+
updated, err := client.Template(context.Background(), template.ID)
488+
require.NoError(t, err)
489+
assert.Equal(t, template.Name, updated.Name)
490+
assert.Equal(t, template.Description, updated.Description)
491+
assert.Equal(t, template.Icon, updated.Icon)
492+
assert.Equal(t, template.DisplayName, updated.DisplayName)
493+
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
494+
assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
495+
assert.Equal(t, template.AllowUserAutostart, updated.AllowUserAutostart)
496+
assert.Equal(t, template.AllowUserAutostop, updated.AllowUserAutostop)
497+
})
498+
499+
t.Run("BlockedNotEntitled", func(t *testing.T) {
500+
t.Parallel()
501+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
502+
user := coderdtest.CreateFirstUser(t, client)
503+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
504+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
505+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
506+
507+
// Make a proxy server that will return a valid entitlements
508+
// response, but without advanced scheduling entitlement.
509+
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
510+
if r.URL.Path == "/api/v2/entitlements" {
511+
res := codersdk.Entitlements{
512+
Features: map[codersdk.FeatureName]codersdk.Feature{},
513+
Warnings: []string{},
514+
Errors: []string{},
515+
HasLicense: true,
516+
Trial: true,
517+
RequireTelemetry: false,
518+
}
519+
for _, feature := range codersdk.FeatureNames {
520+
res.Features[feature] = codersdk.Feature{
521+
Entitlement: codersdk.EntitlementNotEntitled,
522+
Enabled: false,
523+
Limit: nil,
524+
Actual: nil,
525+
}
526+
}
527+
httpapi.Write(r.Context(), w, http.StatusOK, res)
528+
return
529+
}
530+
531+
// Otherwise, proxy the request to the real API server.
532+
httputil.NewSingleHostReverseProxy(client.URL).ServeHTTP(w, r)
533+
}))
534+
defer proxy.Close()
535+
536+
// Create a new client that uses the proxy server.
537+
proxyURL, err := url.Parse(proxy.URL)
538+
require.NoError(t, err)
539+
proxyClient := codersdk.New(proxyURL)
540+
proxyClient.SetSessionToken(client.SessionToken())
541+
542+
// Test the cli command with --allow-user-autostart.
543+
cmdArgs := []string{
544+
"templates",
545+
"edit",
546+
template.Name,
547+
"--allow-user-autostart=false",
548+
}
549+
inv, root := clitest.New(t, cmdArgs...)
550+
clitest.SetupConfig(t, proxyClient, root)
551+
552+
ctx := testutil.Context(t, testutil.WaitLong)
553+
err = inv.WithContext(ctx).Run()
554+
require.Error(t, err)
555+
require.ErrorContains(t, err, "license is not entitled")
556+
557+
// Test the cli command with --allow-user-autostop.
558+
cmdArgs = []string{
559+
"templates",
560+
"edit",
561+
template.Name,
562+
"--allow-user-autostop=false",
563+
}
564+
inv, root = clitest.New(t, cmdArgs...)
565+
clitest.SetupConfig(t, proxyClient, root)
566+
567+
ctx = testutil.Context(t, testutil.WaitLong)
568+
err = inv.WithContext(ctx).Run()
569+
require.Error(t, err)
570+
require.ErrorContains(t, err, "license is not entitled")
571+
431572
// Assert that the template metadata did not change.
432573
updated, err := client.Template(context.Background(), template.ID)
433574
require.NoError(t, err)
@@ -437,6 +578,98 @@ func TestTemplateEdit(t *testing.T) {
437578
assert.Equal(t, template.DisplayName, updated.DisplayName)
438579
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
439580
assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
581+
assert.Equal(t, template.AllowUserAutostart, updated.AllowUserAutostart)
582+
assert.Equal(t, template.AllowUserAutostop, updated.AllowUserAutostop)
583+
})
584+
t.Run("Entitled", func(t *testing.T) {
585+
t.Parallel()
586+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
587+
user := coderdtest.CreateFirstUser(t, client)
588+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
589+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
590+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
591+
592+
// Make a proxy server that will return a valid entitlements
593+
// response, including a valid advanced scheduling entitlement.
594+
var updateTemplateCalled int64
595+
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
596+
if r.URL.Path == "/api/v2/entitlements" {
597+
res := codersdk.Entitlements{
598+
Features: map[codersdk.FeatureName]codersdk.Feature{},
599+
Warnings: []string{},
600+
Errors: []string{},
601+
HasLicense: true,
602+
Trial: true,
603+
RequireTelemetry: false,
604+
}
605+
for _, feature := range codersdk.FeatureNames {
606+
var one int64 = 1
607+
res.Features[feature] = codersdk.Feature{
608+
Entitlement: codersdk.EntitlementNotEntitled,
609+
Enabled: true,
610+
Limit: &one,
611+
Actual: &one,
612+
}
613+
}
614+
httpapi.Write(r.Context(), w, http.StatusOK, res)
615+
return
616+
}
617+
if strings.HasPrefix(r.URL.Path, "/api/v2/templates/") {
618+
body, err := io.ReadAll(r.Body)
619+
require.NoError(t, err)
620+
_ = r.Body.Close()
621+
622+
var req codersdk.UpdateTemplateMeta
623+
err = json.Unmarshal(body, &req)
624+
require.NoError(t, err)
625+
assert.False(t, req.AllowUserAutostart)
626+
assert.False(t, req.AllowUserAutostop)
627+
628+
r.Body = io.NopCloser(bytes.NewReader(body))
629+
atomic.AddInt64(&updateTemplateCalled, 1)
630+
// We still want to call the real route.
631+
}
632+
633+
// Otherwise, proxy the request to the real API server.
634+
httputil.NewSingleHostReverseProxy(client.URL).ServeHTTP(w, r)
635+
}))
636+
defer proxy.Close()
637+
638+
// Create a new client that uses the proxy server.
639+
proxyURL, err := url.Parse(proxy.URL)
640+
require.NoError(t, err)
641+
proxyClient := codersdk.New(proxyURL)
642+
proxyClient.SetSessionToken(client.SessionToken())
643+
644+
// Test the cli command.
645+
cmdArgs := []string{
646+
"templates",
647+
"edit",
648+
template.Name,
649+
"--allow-user-autostart=false",
650+
"--allow-user-autostop=false",
651+
}
652+
inv, root := clitest.New(t, cmdArgs...)
653+
clitest.SetupConfig(t, proxyClient, root)
654+
655+
ctx := testutil.Context(t, testutil.WaitLong)
656+
err = inv.WithContext(ctx).Run()
657+
require.NoError(t, err)
658+
659+
require.EqualValues(t, 1, atomic.LoadInt64(&updateTemplateCalled))
660+
661+
// Assert that the template metadata did not change. We verify the
662+
// correct request gets sent to the server already.
663+
updated, err := client.Template(context.Background(), template.ID)
664+
require.NoError(t, err)
665+
assert.Equal(t, template.Name, updated.Name)
666+
assert.Equal(t, template.Description, updated.Description)
667+
assert.Equal(t, template.Icon, updated.Icon)
668+
assert.Equal(t, template.DisplayName, updated.DisplayName)
669+
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
670+
assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
671+
assert.Equal(t, template.AllowUserAutostart, updated.AllowUserAutostart)
672+
assert.Equal(t, template.AllowUserAutostop, updated.AllowUserAutostop)
440673
})
441674
})
442675
}

cli/testdata/coder_templates_edit_--help.golden

+8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ Usage: coder templates edit [flags] <template>
33
Edit the metadata of a template by name.
44

55
Options
6+
--allow-user-autostart bool (default: true)
7+
Allow users to configure autostart for workspaces on this template.
8+
This can only be disabled in enterprise.
9+
10+
--allow-user-autostop bool (default: true)
11+
Allow users to customize the autostop TTL for workspaces on this
12+
template. This can only be disabled in enterprise.
13+
614
--allow-user-cancel-workspace-jobs bool (default: true)
715
Allow users to cancel in-progress workspace jobs.
816

0 commit comments

Comments
 (0)