mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: allow disabling autostart and custom autostop for template (#6933)
API only, frontend in upcoming PR.
This commit is contained in:
+4
-1
@@ -30,6 +30,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
@@ -72,6 +73,7 @@ import (
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
"github.com/coder/coder/coderd/httpmw"
|
||||
"github.com/coder/coder/coderd/prometheusmetrics"
|
||||
"github.com/coder/coder/coderd/schedule"
|
||||
"github.com/coder/coder/coderd/telemetry"
|
||||
"github.com/coder/coder/coderd/tracing"
|
||||
"github.com/coder/coder/coderd/updatecheck"
|
||||
@@ -632,6 +634,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
||||
LoginRateLimit: loginRateLimit,
|
||||
FilesRateLimit: filesRateLimit,
|
||||
HTTPClient: httpClient,
|
||||
TemplateScheduleStore: &atomic.Pointer[schedule.TemplateScheduleStore]{},
|
||||
SSHConfig: codersdk.SSHConfigResponse{
|
||||
HostnamePrefix: cfg.SSHConfig.DeploymentName.String(),
|
||||
SSHConfigOptions: configSSHOptions,
|
||||
@@ -1019,7 +1022,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
||||
|
||||
autobuildPoller := time.NewTicker(cfg.AutobuildPollInterval.Value())
|
||||
defer autobuildPoller.Stop()
|
||||
autobuildExecutor := executor.New(ctx, options.Database, logger, autobuildPoller.C)
|
||||
autobuildExecutor := executor.New(ctx, options.Database, coderAPI.TemplateScheduleStore, logger, autobuildPoller.C)
|
||||
autobuildExecutor.Run()
|
||||
|
||||
// Currently there is no way to ask the server to shut
|
||||
|
||||
+19
-3
@@ -21,6 +21,8 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
|
||||
defaultTTL time.Duration
|
||||
maxTTL time.Duration
|
||||
allowUserCancelWorkspaceJobs bool
|
||||
allowUserAutostart bool
|
||||
allowUserAutostop bool
|
||||
)
|
||||
client := new(codersdk.Client)
|
||||
|
||||
@@ -32,17 +34,17 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
|
||||
),
|
||||
Short: "Edit the metadata of a template by name.",
|
||||
Handler: func(inv *clibase.Invocation) error {
|
||||
if maxTTL != 0 {
|
||||
if maxTTL != 0 || !allowUserAutostart || !allowUserAutostop {
|
||||
entitlements, err := client.Entitlements(inv.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")
|
||||
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")
|
||||
} 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")
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +66,8 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
|
||||
DefaultTTLMillis: defaultTTL.Milliseconds(),
|
||||
MaxTTLMillis: maxTTL.Milliseconds(),
|
||||
AllowUserCancelWorkspaceJobs: allowUserCancelWorkspaceJobs,
|
||||
AllowUserAutostart: allowUserAutostart,
|
||||
AllowUserAutostop: allowUserAutostop,
|
||||
}
|
||||
|
||||
_, err = client.UpdateTemplateMeta(inv.Context(), template.ID, req)
|
||||
@@ -112,6 +116,18 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
|
||||
Default: "true",
|
||||
Value: clibase.BoolOf(&allowUserCancelWorkspaceJobs),
|
||||
},
|
||||
{
|
||||
Flag: "allow-user-autostart",
|
||||
Description: "Allow users to configure autostart for workspaces on this template. This can only be disabled in enterprise.",
|
||||
Default: "true",
|
||||
Value: clibase.BoolOf(&allowUserAutostart),
|
||||
},
|
||||
{
|
||||
Flag: "allow-user-autostop",
|
||||
Description: "Allow users to customize the autostop TTL for workspaces on this template. This can only be disabled in enterprise.",
|
||||
Default: "true",
|
||||
Value: clibase.BoolOf(&allowUserAutostop),
|
||||
},
|
||||
cliui.SkipPromptOption(),
|
||||
}
|
||||
|
||||
|
||||
+234
-1
@@ -428,7 +428,8 @@ func TestTemplateEdit(t *testing.T) {
|
||||
|
||||
require.EqualValues(t, 1, atomic.LoadInt64(&updateTemplateCalled))
|
||||
|
||||
// Assert that the template metadata did not change.
|
||||
// Assert that the template metadata did not change. We verify the
|
||||
// correct request gets sent to the server already.
|
||||
updated, err := client.Template(context.Background(), template.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, template.Name, updated.Name)
|
||||
@@ -439,4 +440,236 @@ func TestTemplateEdit(t *testing.T) {
|
||||
assert.Equal(t, template.MaxTTLMillis, updated.MaxTTLMillis)
|
||||
})
|
||||
})
|
||||
t.Run("AllowUserScheduling", 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 with --allow-user-autostart.
|
||||
cmdArgs := []string{
|
||||
"templates",
|
||||
"edit",
|
||||
template.Name,
|
||||
"--allow-user-autostart=false",
|
||||
}
|
||||
inv, root := clitest.New(t, cmdArgs...)
|
||||
clitest.SetupConfig(t, client, root)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
err := inv.WithContext(ctx).Run()
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "appears to be an AGPL deployment")
|
||||
|
||||
// Test the cli command with --allow-user-autostop.
|
||||
cmdArgs = []string{
|
||||
"templates",
|
||||
"edit",
|
||||
template.Name,
|
||||
"--allow-user-autostop=false",
|
||||
}
|
||||
inv, root = clitest.New(t, cmdArgs...)
|
||||
clitest.SetupConfig(t, client, root)
|
||||
|
||||
ctx = testutil.Context(t, testutil.WaitLong)
|
||||
err = inv.WithContext(ctx).Run()
|
||||
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)
|
||||
assert.Equal(t, template.AllowUserAutostart, updated.AllowUserAutostart)
|
||||
assert.Equal(t, template.AllowUserAutostop, updated.AllowUserAutostop)
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
// 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,
|
||||
}
|
||||
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 with --allow-user-autostart.
|
||||
cmdArgs := []string{
|
||||
"templates",
|
||||
"edit",
|
||||
template.Name,
|
||||
"--allow-user-autostart=false",
|
||||
}
|
||||
inv, root := clitest.New(t, cmdArgs...)
|
||||
clitest.SetupConfig(t, proxyClient, root)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
err = inv.WithContext(ctx).Run()
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "license is not entitled")
|
||||
|
||||
// Test the cli command with --allow-user-autostop.
|
||||
cmdArgs = []string{
|
||||
"templates",
|
||||
"edit",
|
||||
template.Name,
|
||||
"--allow-user-autostop=false",
|
||||
}
|
||||
inv, root = clitest.New(t, cmdArgs...)
|
||||
clitest.SetupConfig(t, proxyClient, root)
|
||||
|
||||
ctx = testutil.Context(t, testutil.WaitLong)
|
||||
err = inv.WithContext(ctx).Run()
|
||||
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)
|
||||
assert.Equal(t, template.AllowUserAutostart, updated.AllowUserAutostart)
|
||||
assert.Equal(t, template.AllowUserAutostop, updated.AllowUserAutostop)
|
||||
})
|
||||
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)
|
||||
|
||||
// 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,
|
||||
}
|
||||
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.False(t, req.AllowUserAutostart)
|
||||
assert.False(t, req.AllowUserAutostop)
|
||||
|
||||
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,
|
||||
"--allow-user-autostart=false",
|
||||
"--allow-user-autostop=false",
|
||||
}
|
||||
inv, root := clitest.New(t, cmdArgs...)
|
||||
clitest.SetupConfig(t, proxyClient, root)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
err = inv.WithContext(ctx).Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.EqualValues(t, 1, atomic.LoadInt64(&updateTemplateCalled))
|
||||
|
||||
// Assert that the template metadata did not change. We verify the
|
||||
// correct request gets sent to the server already.
|
||||
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)
|
||||
assert.Equal(t, template.AllowUserAutostart, updated.AllowUserAutostart)
|
||||
assert.Equal(t, template.AllowUserAutostop, updated.AllowUserAutostop)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,14 @@ Usage: coder templates edit [flags] <template>
|
||||
Edit the metadata of a template by name.
|
||||
|
||||
[1mOptions[0m
|
||||
--allow-user-autostart bool (default: true)
|
||||
Allow users to configure autostart for workspaces on this template.
|
||||
This can only be disabled in enterprise.
|
||||
|
||||
--allow-user-autostop bool (default: true)
|
||||
Allow users to customize the autostop TTL for workspaces on this
|
||||
template. This can only be disabled in enterprise.
|
||||
|
||||
--allow-user-cancel-workspace-jobs bool (default: true)
|
||||
Allow users to cancel in-progress workspace jobs.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user