Skip to content

feat: add template max_ttl #6114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Mar 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5509824
feat: add template max_ttl
deansheather Feb 8, 2023
4c6a501
chore: split enterprise code for max_ttl into interface
deansheather Feb 15, 2023
7892c5a
Merge branch 'main' into dean/schedule-max-ttl
deansheather Feb 20, 2023
9e37cc7
feat: add API for setting template max_ttl
deansheather Feb 20, 2023
ad64806
working API and dashboard
deansheather Feb 20, 2023
96fd840
working CLI
deansheather Feb 20, 2023
bc39570
Merge branch 'main' into dean/schedule-max-ttl
deansheather Feb 23, 2023
4e1d948
fixup! Merge branch 'main' into dean/schedule-max-ttl
deansheather Feb 23, 2023
69035a6
feat: block disabling auto off if template has max ttl
deansheather Feb 23, 2023
ce7ec39
Merge branch 'main' into dean/schedule-max-ttl
deansheather Mar 1, 2023
89ccfc8
fixup! Merge branch 'main' into dean/schedule-max-ttl
deansheather Mar 1, 2023
feb7b3c
feat: apply template max TTL to workspace TTL on update
deansheather Mar 1, 2023
d54d798
chore: fix tests and differences between sql and memory db
deansheather Mar 1, 2023
6caeb00
Few refactorings for ttl fields
BrunoQuaresma Mar 1, 2023
49a2ec7
Merge branch 'main' into dean/schedule-max-ttl
deansheather Mar 1, 2023
e28e32e
fixup! Merge branch 'main' into dean/schedule-max-ttl
deansheather Mar 1, 2023
651149b
fixup! Merge branch 'main' into dean/schedule-max-ttl
deansheather Mar 1, 2023
25f7d2c
chore: add test for activitybump max_ttl
deansheather Mar 2, 2023
e3d8557
chore: add tests for updating max_ttl on template
deansheather Mar 2, 2023
f62ba67
Merge branch 'main' into dean/schedule-max-ttl
deansheather Mar 2, 2023
2b029d8
fixup! Merge branch 'main' into dean/schedule-max-ttl
deansheather Mar 2, 2023
e60919e
chore: fix security.yaml not having protoc
deansheather Mar 2, 2023
ef3bebf
Merge branch 'main' into dean/schedule-max-ttl
deansheather Mar 2, 2023
351b708
fixup! Merge branch 'main' into dean/schedule-max-ttl
deansheather Mar 2, 2023
847bc4c
chore: move schedule code to new package
deansheather Mar 2, 2023
0611794
fixup! chore: move schedule code to new package
deansheather Mar 2, 2023
e44f589
chore: add alpha label to max_ttl
deansheather Mar 6, 2023
6bb65ac
Merge branch 'main' into dean/schedule-max-ttl
deansheather Mar 6, 2023
a972c57
Merge branch 'main' into dean/schedule-max-ttl
BrunoQuaresma Mar 6, 2023
3ce2bc3
Fix test
BrunoQuaresma Mar 6, 2023
4beff52
fixup! Fix test
deansheather Mar 6, 2023
5e97e96
Fix storybook
BrunoQuaresma Mar 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
working CLI
  • Loading branch information
deansheather committed Feb 20, 2023
commit 96fd84050f5adc41de667ec615fe248f52f0faeb
29 changes: 24 additions & 5 deletions cli/templateedit.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"fmt"
"net/http"
"time"

"github.com/spf13/cobra"
Expand All @@ -18,6 +19,7 @@ func templateEdit() *cobra.Command {
description string
icon string
defaultTTL time.Duration
maxTTL time.Duration
allowUserCancelWorkspaceJobs bool
)

Expand All @@ -30,6 +32,21 @@ func templateEdit() *cobra.Command {
if err != nil {
return xerrors.Errorf("create client: %w", err)
}

if maxTTL != 0 {
entitlements, err := client.Entitlements(cmd.Context())
var sdkErr *codersdk.Error
if xerrors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusNotFound {
return xerrors.Errorf("your deployment appears to be an AGPL deployment, so you cannot set --max-ttl")
} else if err != nil {
return xerrors.Errorf("get entitlements: %w", err)
}

if !entitlements.Features[codersdk.FeatureAdvancedTemplateScheduling].Enabled {
return xerrors.Errorf("your license is not entitled to use advanced template scheduling, so you cannot set --max-ttl")
}
}

organization, err := CurrentOrganization(cmd, client)
if err != nil {
return xerrors.Errorf("get current organization: %w", err)
Expand All @@ -46,6 +63,7 @@ func templateEdit() *cobra.Command {
Description: description,
Icon: icon,
DefaultTTLMillis: defaultTTL.Milliseconds(),
MaxTTLMillis: maxTTL.Milliseconds(),
AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs,
}

Expand All @@ -58,11 +76,12 @@ func templateEdit() *cobra.Command {
},
}

cmd.Flags().StringVarP(&name, "name", "", "", "Edit the template name")
cmd.Flags().StringVarP(&displayName, "display-name", "", "", "Edit the template display name")
cmd.Flags().StringVarP(&description, "description", "", "", "Edit the template description")
cmd.Flags().StringVarP(&icon, "icon", "", "", "Edit the template icon path")
cmd.Flags().DurationVarP(&defaultTTL, "default-ttl", "", 0, "Edit the template default time before shutdown - workspaces created from this template to this value.")
cmd.Flags().StringVarP(&name, "name", "", "", "Edit the template name.")
cmd.Flags().StringVarP(&displayName, "display-name", "", "", "Edit the template display name.")
cmd.Flags().StringVarP(&description, "description", "", "", "Edit the template description.")
cmd.Flags().StringVarP(&icon, "icon", "", "", "Edit the template icon path.")
cmd.Flags().DurationVarP(&defaultTTL, "default-ttl", "", 0, "Edit the template default time before shutdown - workspaces created from this template default to this value.")
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.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an enterprise-only feature

nit: do we need to mention it? If we decide to open this feature to the OSS audience, it would be misleading. I'm not sure what's the rule of thumb for enterprise features in general.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems weird to not mention it if it's a licensed feature, otherwise people will try --max-ttl and see errors. The frontend does something similar, by showing the box but disabling it and adding a "enterprise" label

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it common for other flags? I might have missed it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have done it for deployment config flags before but I'm unsure if we've done it for CLI flags.

cmd.Flags().BoolVarP(&allowUserCancelWorkspaceJobs, "allow-user-cancel-workspace-jobs", "", true, "Allow users to cancel in-progress workspace jobs.")
cliui.AllowSkipPrompt(cmd)

Expand Down
211 changes: 211 additions & 0 deletions cli/templateedit_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
package cli_test

import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
"strconv"
"strings"
"sync/atomic"
"testing"
"time"

Expand All @@ -11,6 +20,7 @@ import (

"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/testutil"
)
Expand Down Expand Up @@ -230,4 +240,205 @@ func TestTemplateEdit(t *testing.T) {
assert.Equal(t, "", updated.Icon)
assert.Equal(t, "", updated.DisplayName)
})
t.Run("MaxTTL", func(t *testing.T) {
t.Parallel()
t.Run("BlockedAGPL", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.DefaultTTLMillis = nil
ctr.MaxTTLMillis = nil
})

// Test the cli command.
cmdArgs := []string{
"templates",
"edit",
template.Name,
"--max-ttl", "1h",
}
cmd, root := clitest.New(t, cmdArgs...)
clitest.SetupConfig(t, client, root)

ctx, _ := testutil.Context(t)
err := cmd.ExecuteContext(ctx)
require.Error(t, err)
require.ErrorContains(t, err, "appears to be an AGPL deployment")

// Assert that the template metadata did not change.
updated, err := client.Template(context.Background(), template.ID)
require.NoError(t, err)
assert.Equal(t, template.Name, updated.Name)
assert.Equal(t, template.Description, updated.Description)
assert.Equal(t, template.Icon, updated.Icon)
assert.Equal(t, template.DisplayName, updated.DisplayName)
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
})

t.Run("BlockedNotEntitled", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.DefaultTTLMillis = nil
ctr.MaxTTLMillis = nil
})

// Make a proxy server that will return a valid entitlements
// response, but without advanced scheduling entitlement.
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v2/entitlements" {
res := codersdk.Entitlements{
Features: map[codersdk.FeatureName]codersdk.Feature{},
Warnings: []string{},
Errors: []string{},
HasLicense: true,
Trial: true,
RequireTelemetry: false,
Experimental: false,
}
for _, feature := range codersdk.FeatureNames {
res.Features[feature] = codersdk.Feature{
Entitlement: codersdk.EntitlementNotEntitled,
Enabled: false,
Limit: nil,
Actual: nil,
}
}
httpapi.Write(r.Context(), w, http.StatusOK, res)
return
}

// Otherwise, proxy the request to the real API server.
httputil.NewSingleHostReverseProxy(client.URL).ServeHTTP(w, r)
}))
defer proxy.Close()

// Create a new client that uses the proxy server.
proxyURL, err := url.Parse(proxy.URL)
require.NoError(t, err)
proxyClient := codersdk.New(proxyURL)
proxyClient.SetSessionToken(client.SessionToken())

// Test the cli command.
cmdArgs := []string{
"templates",
"edit",
template.Name,
"--max-ttl", "1h",
}
cmd, root := clitest.New(t, cmdArgs...)
clitest.SetupConfig(t, proxyClient, root)

ctx, _ := testutil.Context(t)
err = cmd.ExecuteContext(ctx)
require.Error(t, err)
require.ErrorContains(t, err, "license is not entitled")

// Assert that the template metadata did not change.
updated, err := client.Template(context.Background(), template.ID)
require.NoError(t, err)
assert.Equal(t, template.Name, updated.Name)
assert.Equal(t, template.Description, updated.Description)
assert.Equal(t, template.Icon, updated.Icon)
assert.Equal(t, template.DisplayName, updated.DisplayName)
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
})
t.Run("Entitled", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.DefaultTTLMillis = nil
ctr.MaxTTLMillis = nil
})

// Make a proxy server that will return a valid entitlements
// response, including a valid advanced scheduling entitlement.
var updateTemplateCalled int64
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v2/entitlements" {
res := codersdk.Entitlements{
Features: map[codersdk.FeatureName]codersdk.Feature{},
Warnings: []string{},
Errors: []string{},
HasLicense: true,
Trial: true,
RequireTelemetry: false,
Experimental: false,
}
for _, feature := range codersdk.FeatureNames {
var one int64 = 1
res.Features[feature] = codersdk.Feature{
Entitlement: codersdk.EntitlementNotEntitled,
Enabled: true,
Limit: &one,
Actual: &one,
}
}
httpapi.Write(r.Context(), w, http.StatusOK, res)
return
}
if strings.HasPrefix(r.URL.Path, "/api/v2/templates/") {
body, err := io.ReadAll(r.Body)
require.NoError(t, err)
_ = r.Body.Close()

var req codersdk.UpdateTemplateMeta
err = json.Unmarshal(body, &req)
require.NoError(t, err)
assert.Equal(t, time.Hour.Milliseconds(), req.MaxTTLMillis)

r.Body = io.NopCloser(bytes.NewReader(body))
atomic.AddInt64(&updateTemplateCalled, 1)
// We still want to call the real route.
}

// Otherwise, proxy the request to the real API server.
httputil.NewSingleHostReverseProxy(client.URL).ServeHTTP(w, r)
}))
defer proxy.Close()

// Create a new client that uses the proxy server.
proxyURL, err := url.Parse(proxy.URL)
require.NoError(t, err)
proxyClient := codersdk.New(proxyURL)
proxyClient.SetSessionToken(client.SessionToken())

// Test the cli command.
cmdArgs := []string{
"templates",
"edit",
template.Name,
"--max-ttl", "1h",
}
cmd, root := clitest.New(t, cmdArgs...)
clitest.SetupConfig(t, proxyClient, root)

ctx, _ := testutil.Context(t)
err = cmd.ExecuteContext(ctx)
require.NoError(t, err)

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

// Assert that the template metadata did not change.
updated, err := client.Template(context.Background(), template.ID)
require.NoError(t, err)
assert.Equal(t, template.Name, updated.Name)
assert.Equal(t, template.Description, updated.Description)
assert.Equal(t, template.Icon, updated.Icon)
assert.Equal(t, template.DisplayName, updated.DisplayName)
assert.Equal(t, template.DefaultTTLMillis, updated.DefaultTTLMillis)
assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
})
})
}
15 changes: 10 additions & 5 deletions cli/testdata/coder_templates_edit_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ Flags:
--allow-user-cancel-workspace-jobs Allow users to cancel in-progress workspace jobs.
(default true)
--default-ttl duration Edit the template default time before shutdown -
workspaces created from this template to this value.
--description string Edit the template description
--display-name string Edit the template display name
workspaces created from this template default to
this value.
--description string Edit the template description.
--display-name string Edit the template display name.
-h, --help help for edit
--icon string Edit the template icon path
--name string Edit the template name
--icon string Edit the template icon path.
--max-ttl duration 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.
--name string Edit the template name.
-y, --yes Bypass prompts

Global Flags:
Expand Down
8 changes: 6 additions & 2 deletions site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ const defaultInitialValues: CreateTemplateData = {
parameter_values_by_name: undefined,
}

const getInitialValues = (canSetMaxTTL: boolean, starterTemplate?: TemplateExample) => {
const getInitialValues = (
canSetMaxTTL: boolean,
starterTemplate?: TemplateExample,
) => {
let initialValues = defaultInitialValues
if (!canSetMaxTTL) {
initialValues = {
Expand Down Expand Up @@ -97,7 +100,8 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = ({
const styles = useStyles()
const formFooterStyles = useFormFooterStyles()
const { entitlements } = useDashboard()
const canSetMaxTTL = entitlements.features["advanced_template_scheduling"].enabled
const canSetMaxTTL =
entitlements.features["advanced_template_scheduling"].enabled

const form = useFormik<CreateTemplateData>({
initialValues: getInitialValues(canSetMaxTTL, starterTemplate),
Expand Down
Loading