Skip to content

Commit fd26e69

Browse files
committed
add back max_ttl and put restart_requirement behind feature flag
1 parent 0e9437e commit fd26e69

33 files changed

+1017
-112
lines changed

cli/templateedit.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
1919
description string
2020
icon string
2121
defaultTTL time.Duration
22+
maxTTL time.Duration
2223
restartRequirementDaysOfWeek []string
2324
restartRequirementWeeks int64
2425
failureTTL time.Duration
@@ -54,6 +55,7 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
5455
restartRequirementWeeks > 0 ||
5556
!allowUserAutostart ||
5657
!allowUserAutostop ||
58+
maxTTL != 0 ||
5759
failureTTL != 0 ||
5860
inactivityTTL != 0
5961
if requiresEntitlement {
@@ -101,6 +103,7 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
101103
Description: description,
102104
Icon: icon,
103105
DefaultTTLMillis: defaultTTL.Milliseconds(),
106+
MaxTTLMillis: maxTTL.Milliseconds(),
104107
RestartRequirement: &codersdk.TemplateRestartRequirement{
105108
DaysOfWeek: restartRequirementDaysOfWeek,
106109
Weeks: restartRequirementWeeks,
@@ -147,15 +150,24 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
147150
Description: "Edit the template default time before shutdown - workspaces created from this template default to this value.",
148151
Value: clibase.DurationOf(&defaultTTL),
149152
},
153+
{
154+
Flag: "max-ttl",
155+
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.",
156+
Value: clibase.DurationOf(&maxTTL),
157+
},
150158
{
151159
Flag: "restart-requirement-weekdays",
152160
Description: "Edit the template restart requirement weekdays - workspaces created from this template must be restarted on the given weekdays. To unset this value for the template (and disable the restart requirement for the template), pass 'none'.",
153-
Value: clibase.StringArrayOf(&restartRequirementDaysOfWeek),
161+
// TODO(@dean): unhide when we delete max_ttl
162+
Hidden: true,
163+
Value: clibase.StringArrayOf(&restartRequirementDaysOfWeek),
154164
},
155165
{
156166
Flag: "restart-requirement-weeks",
157167
Description: "Edit the template restart requirement weeks - workspaces created from this template must be restarted on an n-weekly basis.",
158-
Value: clibase.Int64Of(&restartRequirementWeeks),
168+
// TODO(@dean): unhide when we delete max_ttl
169+
Hidden: true,
170+
Value: clibase.Int64Of(&restartRequirementWeeks),
159171
},
160172
{
161173
Flag: "failure-ttl",

cli/templateedit_test.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,207 @@ func TestTemplateEdit(t *testing.T) {
519519
assert.Equal(t, template.RestartRequirement.Weeks, updated.RestartRequirement.Weeks)
520520
})
521521
})
522+
// TODO(@dean): remove this test when we remove max_ttl
523+
t.Run("MaxTTL", func(t *testing.T) {
524+
t.Parallel()
525+
t.Run("BlockedAGPL", func(t *testing.T) {
526+
t.Parallel()
527+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
528+
user := coderdtest.CreateFirstUser(t, client)
529+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
530+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
531+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
532+
ctr.DefaultTTLMillis = nil
533+
ctr.MaxTTLMillis = nil
534+
})
535+
536+
// Test the cli command.
537+
cmdArgs := []string{
538+
"templates",
539+
"edit",
540+
template.Name,
541+
"--max-ttl", "1h",
542+
}
543+
inv, root := clitest.New(t, cmdArgs...)
544+
clitest.SetupConfig(t, client, root)
545+
546+
ctx := testutil.Context(t, testutil.WaitLong)
547+
err := inv.WithContext(ctx).Run()
548+
require.Error(t, err)
549+
require.ErrorContains(t, err, "appears to be an AGPL deployment")
550+
551+
// Assert that the template metadata did not change.
552+
updated, err := client.Template(context.Background(), template.ID)
553+
require.NoError(t, err)
554+
assert.Equal(t, template.Name, updated.Name)
555+
assert.Equal(t, template.Description, updated.Description)
556+
assert.Equal(t, template.Icon, updated.Icon)
557+
assert.Equal(t, template.DisplayName, updated.DisplayName)
558+
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
559+
assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
560+
})
561+
562+
t.Run("BlockedNotEntitled", func(t *testing.T) {
563+
t.Parallel()
564+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
565+
user := coderdtest.CreateFirstUser(t, client)
566+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
567+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
568+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
569+
ctr.DefaultTTLMillis = nil
570+
ctr.MaxTTLMillis = nil
571+
})
572+
573+
// Make a proxy server that will return a valid entitlements
574+
// response, but without advanced scheduling entitlement.
575+
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
576+
if r.URL.Path == "/api/v2/entitlements" {
577+
res := codersdk.Entitlements{
578+
Features: map[codersdk.FeatureName]codersdk.Feature{},
579+
Warnings: []string{},
580+
Errors: []string{},
581+
HasLicense: true,
582+
Trial: true,
583+
RequireTelemetry: false,
584+
}
585+
for _, feature := range codersdk.FeatureNames {
586+
res.Features[feature] = codersdk.Feature{
587+
Entitlement: codersdk.EntitlementNotEntitled,
588+
Enabled: false,
589+
Limit: nil,
590+
Actual: nil,
591+
}
592+
}
593+
httpapi.Write(r.Context(), w, http.StatusOK, res)
594+
return
595+
}
596+
597+
// Otherwise, proxy the request to the real API server.
598+
httputil.NewSingleHostReverseProxy(client.URL).ServeHTTP(w, r)
599+
}))
600+
defer proxy.Close()
601+
602+
// Create a new client that uses the proxy server.
603+
proxyURL, err := url.Parse(proxy.URL)
604+
require.NoError(t, err)
605+
proxyClient := codersdk.New(proxyURL)
606+
proxyClient.SetSessionToken(client.SessionToken())
607+
608+
// Test the cli command.
609+
cmdArgs := []string{
610+
"templates",
611+
"edit",
612+
template.Name,
613+
"--max-ttl", "1h",
614+
}
615+
inv, root := clitest.New(t, cmdArgs...)
616+
clitest.SetupConfig(t, proxyClient, root)
617+
618+
ctx := testutil.Context(t, testutil.WaitLong)
619+
err = inv.WithContext(ctx).Run()
620+
require.Error(t, err)
621+
require.ErrorContains(t, err, "license is not entitled")
622+
623+
// Assert that the template metadata did not change.
624+
updated, err := client.Template(context.Background(), template.ID)
625+
require.NoError(t, err)
626+
assert.Equal(t, template.Name, updated.Name)
627+
assert.Equal(t, template.Description, updated.Description)
628+
assert.Equal(t, template.Icon, updated.Icon)
629+
assert.Equal(t, template.DisplayName, updated.DisplayName)
630+
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
631+
assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
632+
})
633+
t.Run("Entitled", func(t *testing.T) {
634+
t.Parallel()
635+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
636+
user := coderdtest.CreateFirstUser(t, client)
637+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
638+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
639+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
640+
ctr.DefaultTTLMillis = nil
641+
ctr.MaxTTLMillis = nil
642+
})
643+
644+
// Make a proxy server that will return a valid entitlements
645+
// response, including a valid advanced scheduling entitlement.
646+
var updateTemplateCalled int64
647+
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
648+
if r.URL.Path == "/api/v2/entitlements" {
649+
res := codersdk.Entitlements{
650+
Features: map[codersdk.FeatureName]codersdk.Feature{},
651+
Warnings: []string{},
652+
Errors: []string{},
653+
HasLicense: true,
654+
Trial: true,
655+
RequireTelemetry: false,
656+
}
657+
for _, feature := range codersdk.FeatureNames {
658+
var one int64 = 1
659+
res.Features[feature] = codersdk.Feature{
660+
Entitlement: codersdk.EntitlementNotEntitled,
661+
Enabled: true,
662+
Limit: &one,
663+
Actual: &one,
664+
}
665+
}
666+
httpapi.Write(r.Context(), w, http.StatusOK, res)
667+
return
668+
}
669+
if strings.HasPrefix(r.URL.Path, "/api/v2/templates/") {
670+
body, err := io.ReadAll(r.Body)
671+
require.NoError(t, err)
672+
_ = r.Body.Close()
673+
674+
var req codersdk.UpdateTemplateMeta
675+
err = json.Unmarshal(body, &req)
676+
require.NoError(t, err)
677+
assert.Equal(t, time.Hour.Milliseconds(), req.MaxTTLMillis)
678+
679+
r.Body = io.NopCloser(bytes.NewReader(body))
680+
atomic.AddInt64(&updateTemplateCalled, 1)
681+
// We still want to call the real route.
682+
}
683+
684+
// Otherwise, proxy the request to the real API server.
685+
httputil.NewSingleHostReverseProxy(client.URL).ServeHTTP(w, r)
686+
}))
687+
defer proxy.Close()
688+
689+
// Create a new client that uses the proxy server.
690+
proxyURL, err := url.Parse(proxy.URL)
691+
require.NoError(t, err)
692+
proxyClient := codersdk.New(proxyURL)
693+
proxyClient.SetSessionToken(client.SessionToken())
694+
695+
// Test the cli command.
696+
cmdArgs := []string{
697+
"templates",
698+
"edit",
699+
template.Name,
700+
"--max-ttl", "1h",
701+
}
702+
inv, root := clitest.New(t, cmdArgs...)
703+
clitest.SetupConfig(t, proxyClient, root)
704+
705+
ctx := testutil.Context(t, testutil.WaitLong)
706+
err = inv.WithContext(ctx).Run()
707+
require.NoError(t, err)
708+
709+
require.EqualValues(t, 1, atomic.LoadInt64(&updateTemplateCalled))
710+
711+
// Assert that the template metadata did not change. We verify the
712+
// correct request gets sent to the server already.
713+
updated, err := client.Template(context.Background(), template.ID)
714+
require.NoError(t, err)
715+
assert.Equal(t, template.Name, updated.Name)
716+
assert.Equal(t, template.Description, updated.Description)
717+
assert.Equal(t, template.Icon, updated.Icon)
718+
assert.Equal(t, template.DisplayName, updated.DisplayName)
719+
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
720+
assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
721+
})
722+
})
522723
t.Run("AllowUserScheduling", func(t *testing.T) {
523724
t.Parallel()
524725
t.Run("BlockedAGPL", func(t *testing.T) {

cli/testdata/coder_templates_edit_--help.golden

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,14 @@ Edit the metadata of a template by name.
3535
Specify an inactivity TTL for workspaces created from this template.
3636
This licensed feature's default is 0h (off).
3737

38+
--max-ttl duration
39+
Edit the template maximum time before shutdown - workspaces created
40+
from this template must shutdown within the given duration after
41+
starting. This is an enterprise-only feature.
42+
3843
--name string
3944
Edit the template name.
4045

41-
--restart-requirement-weekdays string-array
42-
Edit the template restart requirement weekdays - workspaces created
43-
from this template must be restarted on the given weekdays. To unset
44-
this value for the template (and disable the restart requirement for
45-
the template), pass 'none'.
46-
47-
--restart-requirement-weeks int
48-
Edit the template restart requirement weeks - workspaces created from
49-
this template must be restarted on an n-weekly basis.
50-
5146
-y, --yes bool
5247
Bypass prompts.
5348

coderd/apidoc/docs.go

Lines changed: 12 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 12 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbfake/dbfake.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4745,6 +4745,7 @@ func (q *FakeQuerier) UpdateTemplateScheduleByID(_ context.Context, arg database
47454745
tpl.AllowUserAutostop = arg.AllowUserAutostop
47464746
tpl.UpdatedAt = database.Now()
47474747
tpl.DefaultTTL = arg.DefaultTTL
4748+
tpl.MaxTTL = arg.MaxTTL
47484749
tpl.RestartRequirementDaysOfWeek = arg.RestartRequirementDaysOfWeek
47494750
tpl.RestartRequirementWeeks = arg.RestartRequirementWeeks
47504751
tpl.FailureTTL = arg.FailureTTL

coderd/database/dump.sql

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
ALTER TABLE templates
22
DROP COLUMN restart_requirement_days_of_week,
3-
DROP COLUMN restart_requirement_weeks,
4-
ADD COLUMN max_ttl bigint NOT NULL DEFAULT 0;
3+
DROP COLUMN restart_requirement_weeks;

coderd/database/migrations/000139_template_restart_requirement.up.sql

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
BEGIN;
22

33
ALTER TABLE templates
4-
DROP COLUMN max_ttl,
4+
-- The max_ttl column will be dropped eventually when the new "restart
5+
-- requirement" feature flag is fully rolled out.
6+
-- DROP COLUMN max_ttl,
57
ADD COLUMN restart_requirement_days_of_week smallint NOT NULL DEFAULT 0,
68
ADD COLUMN restart_requirement_weeks bigint NOT NULL DEFAULT 0;
79

0 commit comments

Comments
 (0)