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

Lines changed: 4 additions & 1 deletion
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

Lines changed: 19 additions & 3 deletions
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

Lines changed: 233 additions & 0 deletions
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

Lines changed: 8 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy