Files
coder/cli/templateedit.go
T
Ethan 78b5a0f5a2 feat(cli): add --agents-allowed to template commands (#27517)
Relates to CODAGT-713

Depends on #27515

This adds `--agents-allowed` to `coder templates create` and `coder templates edit`. Template creation defaults the option to true, matching the per-template API and database default, while template editing only changes the value when the flag is explicitly supplied so unrelated edits preserve the existing setting.

The generated CLI help and reference documentation include the new option. #27518 updates the Coder Agents platform controls documentation to describe the completed per-template model.
2026-08-06 14:45:38 +10:00

344 lines
13 KiB
Go

package cli
import (
"fmt"
"net/http"
"time"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
func (r *RootCmd) templateEdit() *serpent.Command {
const deprecatedFlagName = "deprecated"
var (
name string
displayName string
description string
icon string
defaultTTL time.Duration
activityBump time.Duration
timeTilAutostopNotify time.Duration
autostopRequirementDaysOfWeek []string
autostopRequirementWeeks int64
autostartRequirementDaysOfWeek []string
failureTTL time.Duration
dormancyThreshold time.Duration
dormancyAutoDeletion time.Duration
allowUserCancelWorkspaceJobs bool
allowUserAutostart bool
allowUserAutostop bool
agentsAllowed bool
requireActiveVersion bool
deprecationMessage string
disableEveryone bool
orgContext = NewOrganizationContext()
)
cmd := &serpent.Command{
Use: "edit <template>",
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
),
Short: "Edit the metadata of a template by name.",
Handler: func(inv *serpent.Invocation) error {
client, err := r.InitClient(inv)
if err != nil {
return err
}
unsetAutostopRequirementDaysOfWeek := len(autostopRequirementDaysOfWeek) == 1 && autostopRequirementDaysOfWeek[0] == "none"
requiresScheduling := (len(autostopRequirementDaysOfWeek) > 0 && !unsetAutostopRequirementDaysOfWeek) ||
autostopRequirementWeeks > 0 ||
!allowUserAutostart ||
!allowUserAutostop ||
failureTTL != 0 ||
dormancyThreshold != 0 ||
dormancyAutoDeletion != 0 ||
len(autostartRequirementDaysOfWeek) > 0
requiresEntitlement := requiresScheduling || requireActiveVersion
if requiresEntitlement {
entitlements, err := client.Entitlements(inv.Context())
if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusNotFound {
return xerrors.Errorf("your deployment appears to be an AGPL deployment, so you cannot set enterprise-only flags")
} else if err != nil {
return xerrors.Errorf("get entitlements: %w", err)
}
if requiresScheduling && !entitlements.Features[codersdk.FeatureAdvancedTemplateScheduling].Enabled {
return xerrors.Errorf("your license is not entitled to use advanced template scheduling, so you cannot set --failure-ttl, --inactivityTTL, --allow-user-autostart=false or --allow-user-autostop=false")
}
if requireActiveVersion {
if !entitlements.Features[codersdk.FeatureAccessControl].Enabled {
return xerrors.Errorf("your license is not entitled to use enterprise access control, so you cannot set --require-active-version")
}
}
}
organization, err := orgContext.Selected(inv, client)
if err != nil {
return xerrors.Errorf("get current organization: %w", err)
}
template, err := client.TemplateByName(inv.Context(), organization.ID, inv.Args[0])
if err != nil {
return xerrors.Errorf("get workspace template: %w", err)
}
// Default values
if !userSetOption(inv, "name") {
name = template.Name
}
if !userSetOption(inv, "description") {
description = template.Description
}
if !userSetOption(inv, "icon") {
icon = template.Icon
}
if !userSetOption(inv, "display-name") {
displayName = template.DisplayName
}
if !userSetOption(inv, "default-ttl") {
defaultTTL = time.Duration(template.DefaultTTLMillis) * time.Millisecond
}
if !userSetOption(inv, "activity-bump") {
activityBump = time.Duration(template.ActivityBumpMillis) * time.Millisecond
}
if !userSetOption(inv, "autostop-reminder") {
timeTilAutostopNotify = time.Duration(template.TimeTilAutostopNotifyMillis) * time.Millisecond
}
if !userSetOption(inv, "allow-user-autostop") {
allowUserAutostop = template.AllowUserAutostop
}
if !userSetOption(inv, "allow-user-autostart") {
allowUserAutostart = template.AllowUserAutostart
}
if !userSetOption(inv, "allow-user-cancel-workspace-jobs") {
allowUserCancelWorkspaceJobs = template.AllowUserCancelWorkspaceJobs
}
if !userSetOption(inv, "failure-ttl") {
failureTTL = time.Duration(template.FailureTTLMillis) * time.Millisecond
}
if !userSetOption(inv, "dormancy-threshold") {
dormancyThreshold = time.Duration(template.TimeTilDormantMillis) * time.Millisecond
}
if !userSetOption(inv, "dormancy-auto-deletion") {
dormancyAutoDeletion = time.Duration(template.TimeTilDormantAutoDeleteMillis) * time.Millisecond
}
if !userSetOption(inv, "agents-allowed") {
agentsAllowed = template.AgentsAllowed
}
if !userSetOption(inv, "require-active-version") {
requireActiveVersion = template.RequireActiveVersion
}
if !userSetOption(inv, "autostop-requirement-weekdays") {
autostopRequirementDaysOfWeek = template.AutostopRequirement.DaysOfWeek
}
if unsetAutostopRequirementDaysOfWeek {
autostopRequirementDaysOfWeek = []string{}
}
if !userSetOption(inv, "autostop-requirement-weeks") {
autostopRequirementWeeks = template.AutostopRequirement.Weeks
}
switch {
case len(autostartRequirementDaysOfWeek) == 1 && autostartRequirementDaysOfWeek[0] == "all":
// Set it to every day of the week
autostartRequirementDaysOfWeek = []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"}
case !userSetOption(inv, "autostart-requirement-weekdays"):
autostartRequirementDaysOfWeek = template.AutostartRequirement.DaysOfWeek
case len(autostartRequirementDaysOfWeek) == 0:
autostartRequirementDaysOfWeek = []string{}
}
var deprecated *string
if userSetOption(inv, "deprecated") {
deprecated = &deprecationMessage
}
var disableEveryoneGroup bool
if userSetOption(inv, "private") {
disableEveryoneGroup = disableEveryone
}
req := codersdk.UpdateTemplateMeta{
Name: &name,
DisplayName: &displayName,
Description: &description,
Icon: &icon,
DefaultTTLMillis: ptr.Ref(defaultTTL.Milliseconds()),
ActivityBumpMillis: ptr.Ref(activityBump.Milliseconds()),
TimeTilAutostopNotifyMillis: ptr.Ref(timeTilAutostopNotify.Milliseconds()),
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
DaysOfWeek: autostopRequirementDaysOfWeek,
Weeks: autostopRequirementWeeks,
},
AutostartRequirement: &codersdk.TemplateAutostartRequirement{
DaysOfWeek: autostartRequirementDaysOfWeek,
},
FailureTTLMillis: ptr.Ref(failureTTL.Milliseconds()),
TimeTilDormantMillis: ptr.Ref(dormancyThreshold.Milliseconds()),
TimeTilDormantAutoDeleteMillis: ptr.Ref(dormancyAutoDeletion.Milliseconds()),
AllowUserCancelWorkspaceJobs: &allowUserCancelWorkspaceJobs,
AllowUserAutostart: &allowUserAutostart,
AllowUserAutostop: &allowUserAutostop,
AgentsAllowed: &agentsAllowed,
RequireActiveVersion: &requireActiveVersion,
DeprecationMessage: deprecated,
DisableEveryoneGroupAccess: &disableEveryoneGroup,
// TODO(Emyrk): now that the API accepts partial updates,
// rewrite this CLI to only set pointers for flags the user
// explicitly provided via userSetOption. The current
// fetch-then-resend-everything dance is no longer required.
}
_, err = client.UpdateTemplateMeta(inv.Context(), template.ID, req)
if err != nil {
return xerrors.Errorf("update template metadata: %w", err)
}
_, _ = fmt.Fprintf(inv.Stdout, "Updated template metadata at %s!\n", pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp)))
return nil
},
}
cmd.Options = serpent.OptionSet{
{
Flag: "name",
Description: "Edit the template name.",
Value: serpent.StringOf(&name),
},
{
Flag: "display-name",
Description: "Edit the template display name.",
Value: serpent.StringOf(&displayName),
},
{
Flag: "description",
Description: "Edit the template description.",
Value: serpent.StringOf(&description),
},
{
Name: deprecatedFlagName,
Flag: "deprecated",
Description: "Sets the template as deprecated. Must be a message explaining why the template is deprecated.",
Value: serpent.StringOf(&deprecationMessage),
},
{
Flag: "icon",
Description: "Edit the template icon path.",
Value: serpent.StringOf(&icon),
},
{
Flag: "default-ttl",
Description: "Edit the template default time before shutdown - workspaces created from this template default to this value. Maps to \"Default autostop\" in the UI.",
Value: serpent.DurationOf(&defaultTTL),
},
{
Flag: "activity-bump",
Description: "Edit the template activity bump - workspaces created from this template will have their shutdown time bumped by this value when activity is detected. Maps to \"Activity bump\" in the UI.",
Value: serpent.DurationOf(&activityBump),
},
{
Flag: "autostop-reminder",
Description: "Edit how long before the autostop deadline a reminder notification is sent for workspaces created from this template, in Go duration format (e.g. 1h, 30m). Set to 0 to disable.",
Value: serpent.DurationOf(&timeTilAutostopNotify),
},
{
Flag: "autostart-requirement-weekdays",
Description: "Edit the template autostart requirement weekdays - workspaces created from this template can only autostart on the given weekdays. To unset this value for the template (and allow autostart on all days), pass 'all'.",
Value: serpent.EnumArrayOf(&autostartRequirementDaysOfWeek, append(codersdk.AllDaysOfWeek, "all")...),
},
{
Flag: "autostop-requirement-weekdays",
Description: "Edit the template autostop requirement weekdays - workspaces created from this template must be restarted on the given weekdays. To unset this value for the template (and disable the autostop requirement for the template), pass 'none'.",
Value: serpent.EnumArrayOf(&autostopRequirementDaysOfWeek, append(codersdk.AllDaysOfWeek, "none")...),
},
{
Flag: "autostop-requirement-weeks",
Description: "Edit the template autostop requirement weeks - workspaces created from this template must be restarted on an n-weekly basis.",
Value: serpent.Int64Of(&autostopRequirementWeeks),
},
{
Flag: "failure-ttl",
Description: "Specify a failure TTL for workspaces created from this template. It is the amount of time after a failed \"start\" build before coder automatically schedules a \"stop\" build to cleanup.This licensed feature's default is 0h (off). Maps to \"Failure cleanup\" in the UI.",
Default: "0h",
Value: serpent.DurationOf(&failureTTL),
},
{
Flag: "dormancy-threshold",
Description: "Specify a duration workspaces may be inactive prior to being moved to the dormant state. This licensed feature's default is 0h (off). Maps to \"Dormancy threshold\" in the UI.",
Default: "0h",
Value: serpent.DurationOf(&dormancyThreshold),
},
{
Flag: "dormancy-auto-deletion",
Description: "Specify a duration workspaces may be in the dormant state prior to being deleted. This licensed feature's default is 0h (off). Maps to \"Dormancy Auto-Deletion\" in the UI.",
Default: "0h",
Value: serpent.DurationOf(&dormancyAutoDeletion),
},
{
Flag: "agents-allowed",
Description: "Allow Coder Agents to create workspaces using this template.",
Default: "true",
Value: serpent.BoolOf(&agentsAllowed),
},
{
Flag: "allow-user-cancel-workspace-jobs",
Description: "Allow users to cancel in-progress workspace jobs.",
Default: "true",
Value: serpent.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: serpent.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: serpent.BoolOf(&allowUserAutostop),
},
{
Flag: "require-active-version",
Description: "Requires workspace builds to use the active template version. This setting does not apply to template admins. This is an enterprise-only feature. See https://coder.com/docs/admin/templates/managing-templates#require-automatic-updates-enterprise for more details.",
Value: serpent.BoolOf(&requireActiveVersion),
Default: "false",
},
{
Flag: "private",
Description: "Disable the default behavior of granting template access to the 'everyone' group. " +
"The template permissions must be updated to allow non-admin users to use this template.",
Value: serpent.BoolOf(&disableEveryone),
Default: "false",
},
cliui.SkipPromptOption(),
}
orgContext.AttachOptions(cmd)
return cmd
}