Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 66a6b59

Browse files
feat: add template max_ttl (#6114)
Co-authored-by: Bruno Quaresma <[email protected]>
1 parent 248c53d commit 66a6b59

File tree

71 files changed

+2088
-396
lines changed

Some content is hidden

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

71 files changed

+2088
-396
lines changed

.github/workflows/ci.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,9 @@ jobs:
186186

187187
- name: Install Protoc
188188
run: |
189-
# protoc must be in lockstep with our dogfood Dockerfile
190-
# or the version in the comments will differ.
189+
# protoc must be in lockstep with our dogfood Dockerfile or the
190+
# version in the comments will differ. This is also defined in
191+
# security.yaml
191192
set -x
192193
cd dogfood
193194
DOCKER_BUILDKIT=1 docker build . --target proto -t protoc

.github/workflows/security.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,22 @@ jobs:
9999
100100
- name: Install yq
101101
run: go run github.com/mikefarah/yq/[email protected]
102+
- name: Install protoc-gen-go
103+
run: go install google.golang.org/protobuf/cmd/[email protected]
104+
- name: Install protoc-gen-go-drpc
105+
run: go install storj.io/drpc/cmd/[email protected]
106+
- name: Install Protoc
107+
run: |
108+
# protoc must be in lockstep with our dogfood Dockerfile or the
109+
# version in the comments will differ. This is also defined in
110+
# ci.yaml.
111+
set -x
112+
cd dogfood
113+
DOCKER_BUILDKIT=1 docker build . --target proto -t protoc
114+
protoc_path=/usr/local/bin/protoc
115+
docker run --rm --entrypoint cat protoc /tmp/bin/protoc > $protoc_path
116+
chmod +x $protoc_path
117+
protoc --version
102118
103119
- name: Build Coder linux amd64 Docker image
104120
id: build

cli/list.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"github.com/spf13/cobra"
99

1010
"github.com/coder/coder/cli/cliui"
11-
"github.com/coder/coder/coderd/autobuild/schedule"
11+
"github.com/coder/coder/coderd/schedule"
1212
"github.com/coder/coder/coderd/util/ptr"
1313
"github.com/coder/coder/codersdk"
1414
)

cli/schedule.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"golang.org/x/xerrors"
1111

1212
"github.com/coder/coder/cli/cliui"
13-
"github.com/coder/coder/coderd/autobuild/schedule"
13+
"github.com/coder/coder/coderd/schedule"
1414
"github.com/coder/coder/coderd/util/ptr"
1515
"github.com/coder/coder/coderd/util/tz"
1616
"github.com/coder/coder/codersdk"

cli/schedule_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,13 +308,16 @@ func TestScheduleOverride(t *testing.T) {
308308
user = coderdtest.CreateFirstUser(t, client)
309309
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
310310
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
311-
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
312-
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
311+
template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
312+
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
313313
cwr.TTLMillis = nil
314314
})
315315
cmdArgs = []string{"schedule", "override-stop", workspace.Name, "1h"}
316316
stdoutBuf = &bytes.Buffer{}
317317
)
318+
require.Zero(t, template.DefaultTTLMillis)
319+
require.Zero(t, template.MaxTTLMillis)
320+
318321
// Unset the workspace TTL
319322
err = client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{TTLMillis: nil})
320323
require.NoError(t, err)

cli/templateedit.go

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli
22

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

78
"github.com/spf13/cobra"
@@ -18,6 +19,7 @@ func templateEdit() *cobra.Command {
1819
description string
1920
icon string
2021
defaultTTL time.Duration
22+
maxTTL time.Duration
2123
allowUserCancelWorkspaceJobs bool
2224
)
2325

@@ -30,6 +32,21 @@ func templateEdit() *cobra.Command {
3032
if err != nil {
3133
return xerrors.Errorf("create client: %w", err)
3234
}
35+
36+
if maxTTL != 0 {
37+
entitlements, err := client.Entitlements(cmd.Context())
38+
var sdkErr *codersdk.Error
39+
if xerrors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusNotFound {
40+
return xerrors.Errorf("your deployment appears to be an AGPL deployment, so you cannot set --max-ttl")
41+
} else if err != nil {
42+
return xerrors.Errorf("get entitlements: %w", err)
43+
}
44+
45+
if !entitlements.Features[codersdk.FeatureAdvancedTemplateScheduling].Enabled {
46+
return xerrors.Errorf("your license is not entitled to use advanced template scheduling, so you cannot set --max-ttl")
47+
}
48+
}
49+
3350
organization, err := CurrentOrganization(cmd, client)
3451
if err != nil {
3552
return xerrors.Errorf("get current organization: %w", err)
@@ -46,6 +63,7 @@ func templateEdit() *cobra.Command {
4663
Description: description,
4764
Icon: icon,
4865
DefaultTTLMillis: defaultTTL.Milliseconds(),
66+
MaxTTLMillis: maxTTL.Milliseconds(),
4967
AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs,
5068
}
5169

@@ -58,11 +76,12 @@ func templateEdit() *cobra.Command {
5876
},
5977
}
6078

61-
cmd.Flags().StringVarP(&name, "name", "", "", "Edit the template name")
62-
cmd.Flags().StringVarP(&displayName, "display-name", "", "", "Edit the template display name")
63-
cmd.Flags().StringVarP(&description, "description", "", "", "Edit the template description")
64-
cmd.Flags().StringVarP(&icon, "icon", "", "", "Edit the template icon path")
65-
cmd.Flags().DurationVarP(&defaultTTL, "default-ttl", "", 0, "Edit the template default time before shutdown - workspaces created from this template to this value.")
79+
cmd.Flags().StringVarP(&name, "name", "", "", "Edit the template name.")
80+
cmd.Flags().StringVarP(&displayName, "display-name", "", "", "Edit the template display name.")
81+
cmd.Flags().StringVarP(&description, "description", "", "", "Edit the template description.")
82+
cmd.Flags().StringVarP(&icon, "icon", "", "", "Edit the template icon path.")
83+
cmd.Flags().DurationVarP(&defaultTTL, "default-ttl", "", 0, "Edit the template default time before shutdown - workspaces created from this template default to this value.")
84+
cmd.Flags().DurationVarP(&maxTTL, "max-ttl", "", 0, "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.")
6685
cmd.Flags().BoolVarP(&allowUserCancelWorkspaceJobs, "allow-user-cancel-workspace-jobs", "", true, "Allow users to cancel in-progress workspace jobs.")
6786
cliui.AllowSkipPrompt(cmd)
6887

cli/templateedit_test.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
package cli_test
22

33
import (
4+
"bytes"
45
"context"
6+
"encoding/json"
7+
"io"
8+
"net/http"
9+
"net/http/httptest"
10+
"net/http/httputil"
11+
"net/url"
512
"strconv"
13+
"strings"
14+
"sync/atomic"
615
"testing"
716
"time"
817

@@ -11,6 +20,7 @@ import (
1120

1221
"github.com/coder/coder/cli/clitest"
1322
"github.com/coder/coder/coderd/coderdtest"
23+
"github.com/coder/coder/coderd/httpapi"
1424
"github.com/coder/coder/codersdk"
1525
"github.com/coder/coder/testutil"
1626
)
@@ -230,4 +240,205 @@ func TestTemplateEdit(t *testing.T) {
230240
assert.Equal(t, "", updated.Icon)
231241
assert.Equal(t, "", updated.DisplayName)
232242
})
243+
t.Run("MaxTTL", func(t *testing.T) {
244+
t.Parallel()
245+
t.Run("BlockedAGPL", func(t *testing.T) {
246+
t.Parallel()
247+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
248+
user := coderdtest.CreateFirstUser(t, client)
249+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
250+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
251+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
252+
ctr.DefaultTTLMillis = nil
253+
ctr.MaxTTLMillis = nil
254+
})
255+
256+
// Test the cli command.
257+
cmdArgs := []string{
258+
"templates",
259+
"edit",
260+
template.Name,
261+
"--max-ttl", "1h",
262+
}
263+
cmd, root := clitest.New(t, cmdArgs...)
264+
clitest.SetupConfig(t, client, root)
265+
266+
ctx, _ := testutil.Context(t)
267+
err := cmd.ExecuteContext(ctx)
268+
require.Error(t, err)
269+
require.ErrorContains(t, err, "appears to be an AGPL deployment")
270+
271+
// Assert that the template metadata did not change.
272+
updated, err := client.Template(context.Background(), template.ID)
273+
require.NoError(t, err)
274+
assert.Equal(t, template.Name, updated.Name)
275+
assert.Equal(t, template.Description, updated.Description)
276+
assert.Equal(t, template.Icon, updated.Icon)
277+
assert.Equal(t, template.DisplayName, updated.DisplayName)
278+
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
279+
assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
280+
})
281+
282+
t.Run("BlockedNotEntitled", func(t *testing.T) {
283+
t.Parallel()
284+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
285+
user := coderdtest.CreateFirstUser(t, client)
286+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
287+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
288+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
289+
ctr.DefaultTTLMillis = nil
290+
ctr.MaxTTLMillis = nil
291+
})
292+
293+
// Make a proxy server that will return a valid entitlements
294+
// response, but without advanced scheduling entitlement.
295+
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
296+
if r.URL.Path == "/api/v2/entitlements" {
297+
res := codersdk.Entitlements{
298+
Features: map[codersdk.FeatureName]codersdk.Feature{},
299+
Warnings: []string{},
300+
Errors: []string{},
301+
HasLicense: true,
302+
Trial: true,
303+
RequireTelemetry: false,
304+
Experimental: false,
305+
}
306+
for _, feature := range codersdk.FeatureNames {
307+
res.Features[feature] = codersdk.Feature{
308+
Entitlement: codersdk.EntitlementNotEntitled,
309+
Enabled: false,
310+
Limit: nil,
311+
Actual: nil,
312+
}
313+
}
314+
httpapi.Write(r.Context(), w, http.StatusOK, res)
315+
return
316+
}
317+
318+
// Otherwise, proxy the request to the real API server.
319+
httputil.NewSingleHostReverseProxy(client.URL).ServeHTTP(w, r)
320+
}))
321+
defer proxy.Close()
322+
323+
// Create a new client that uses the proxy server.
324+
proxyURL, err := url.Parse(proxy.URL)
325+
require.NoError(t, err)
326+
proxyClient := codersdk.New(proxyURL)
327+
proxyClient.SetSessionToken(client.SessionToken())
328+
329+
// Test the cli command.
330+
cmdArgs := []string{
331+
"templates",
332+
"edit",
333+
template.Name,
334+
"--max-ttl", "1h",
335+
}
336+
cmd, root := clitest.New(t, cmdArgs...)
337+
clitest.SetupConfig(t, proxyClient, root)
338+
339+
ctx, _ := testutil.Context(t)
340+
err = cmd.ExecuteContext(ctx)
341+
require.Error(t, err)
342+
require.ErrorContains(t, err, "license is not entitled")
343+
344+
// Assert that the template metadata did not change.
345+
updated, err := client.Template(context.Background(), template.ID)
346+
require.NoError(t, err)
347+
assert.Equal(t, template.Name, updated.Name)
348+
assert.Equal(t, template.Description, updated.Description)
349+
assert.Equal(t, template.Icon, updated.Icon)
350+
assert.Equal(t, template.DisplayName, updated.DisplayName)
351+
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
352+
assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
353+
})
354+
t.Run("Entitled", func(t *testing.T) {
355+
t.Parallel()
356+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
357+
user := coderdtest.CreateFirstUser(t, client)
358+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
359+
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
360+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
361+
ctr.DefaultTTLMillis = nil
362+
ctr.MaxTTLMillis = nil
363+
})
364+
365+
// Make a proxy server that will return a valid entitlements
366+
// response, including a valid advanced scheduling entitlement.
367+
var updateTemplateCalled int64
368+
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
369+
if r.URL.Path == "/api/v2/entitlements" {
370+
res := codersdk.Entitlements{
371+
Features: map[codersdk.FeatureName]codersdk.Feature{},
372+
Warnings: []string{},
373+
Errors: []string{},
374+
HasLicense: true,
375+
Trial: true,
376+
RequireTelemetry: false,
377+
Experimental: false,
378+
}
379+
for _, feature := range codersdk.FeatureNames {
380+
var one int64 = 1
381+
res.Features[feature] = codersdk.Feature{
382+
Entitlement: codersdk.EntitlementNotEntitled,
383+
Enabled: true,
384+
Limit: &one,
385+
Actual: &one,
386+
}
387+
}
388+
httpapi.Write(r.Context(), w, http.StatusOK, res)
389+
return
390+
}
391+
if strings.HasPrefix(r.URL.Path, "/api/v2/templates/") {
392+
body, err := io.ReadAll(r.Body)
393+
require.NoError(t, err)
394+
_ = r.Body.Close()
395+
396+
var req codersdk.UpdateTemplateMeta
397+
err = json.Unmarshal(body, &req)
398+
require.NoError(t, err)
399+
assert.Equal(t, time.Hour.Milliseconds(), req.MaxTTLMillis)
400+
401+
r.Body = io.NopCloser(bytes.NewReader(body))
402+
atomic.AddInt64(&updateTemplateCalled, 1)
403+
// We still want to call the real route.
404+
}
405+
406+
// Otherwise, proxy the request to the real API server.
407+
httputil.NewSingleHostReverseProxy(client.URL).ServeHTTP(w, r)
408+
}))
409+
defer proxy.Close()
410+
411+
// Create a new client that uses the proxy server.
412+
proxyURL, err := url.Parse(proxy.URL)
413+
require.NoError(t, err)
414+
proxyClient := codersdk.New(proxyURL)
415+
proxyClient.SetSessionToken(client.SessionToken())
416+
417+
// Test the cli command.
418+
cmdArgs := []string{
419+
"templates",
420+
"edit",
421+
template.Name,
422+
"--max-ttl", "1h",
423+
}
424+
cmd, root := clitest.New(t, cmdArgs...)
425+
clitest.SetupConfig(t, proxyClient, root)
426+
427+
ctx, _ := testutil.Context(t)
428+
err = cmd.ExecuteContext(ctx)
429+
require.NoError(t, err)
430+
431+
require.EqualValues(t, 1, atomic.LoadInt64(&updateTemplateCalled))
432+
433+
// Assert that the template metadata did not change.
434+
updated, err := client.Template(context.Background(), template.ID)
435+
require.NoError(t, err)
436+
assert.Equal(t, template.Name, updated.Name)
437+
assert.Equal(t, template.Description, updated.Description)
438+
assert.Equal(t, template.Icon, updated.Icon)
439+
assert.Equal(t, template.DisplayName, updated.DisplayName)
440+
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
441+
assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
442+
})
443+
})
233444
}

cli/testdata/coder_list_--output_json.golden

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"reason": "initiator",
4040
"resources": [],
4141
"deadline": "[timestamp]",
42+
"max_deadline": null,
4243
"status": "running",
4344
"daily_cost": 0
4445
},

cli/testdata/coder_templates_edit_--help.golden

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@ Flags:
77
--allow-user-cancel-workspace-jobs Allow users to cancel in-progress workspace jobs.
88
(default true)
99
--default-ttl duration Edit the template default time before shutdown -
10-
workspaces created from this template to this value.
11-
--description string Edit the template description
12-
--display-name string Edit the template display name
10+
workspaces created from this template default to
11+
this value.
12+
--description string Edit the template description.
13+
--display-name string Edit the template display name.
1314
-h, --help help for edit
14-
--icon string Edit the template icon path
15-
--name string Edit the template name
15+
--icon string Edit the template icon path.
16+
--max-ttl duration Edit the template maximum time before shutdown -
17+
workspaces created from this template must shutdown
18+
within the given duration after starting. This is
19+
an enterprise-only feature.
20+
--name string Edit the template name.
1621
-y, --yes Bypass prompts
1722

1823
Global Flags:

0 commit comments

Comments
 (0)