chore: deprecate template create command in favor of template push (#11390)

This commit is contained in:
Garrett Delfosse 2024-01-05 16:04:14 -05:00 committed by GitHub
parent 3d54bc06f6
commit b21da38bea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 498 additions and 420 deletions

View File

@ -416,7 +416,7 @@ jobs:
# Create template
cd ./.github/pr-deployments/template
coder templates create -y --variable namespace=pr${{ env.PR_NUMBER }} kubernetes
coder templates push -y --variable namespace=pr${{ env.PR_NUMBER }} kubernetes
# Create workspace
coder create --template="kubernetes" kube --parameter cpu=2 --parameter memory=4 --parameter home_disk_size=2 -y

21
cli/cliui/deprecation.go Normal file
View File

@ -0,0 +1,21 @@
package cliui
import (
"fmt"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/pretty"
)
func DeprecationWarning(message string) clibase.MiddlewareFunc {
return func(next clibase.HandlerFunc) clibase.HandlerFunc {
return func(i *clibase.Invocation) error {
_, _ = fmt.Fprintln(i.Stdout, "\n"+pretty.Sprint(DefaultStyles.Wrap,
pretty.Sprint(
DefaultStyles.Warn,
"DEPRECATION WARNING: This command will be removed in a future release."+"\n"+message+"\n"),
))
return next(i)
}
}
}

View File

@ -1,15 +1,11 @@
package cli
import (
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"unicode/utf8"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/pretty"
@ -40,9 +36,13 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
client := new(codersdk.Client)
cmd := &clibase.Cmd{
Use: "create [name]",
Short: "Create a template from the current directory or as specified by flag",
Short: "DEPRECATED: Create a template from the current directory or as specified by flag",
Middleware: clibase.Chain(
clibase.RequireRangeArgs(0, 1),
cliui.DeprecationWarning(
"Use `coder templates push` command for creating and updating templates. \n"+
"Use `coder templates edit` command for editing template settings. ",
),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
@ -253,107 +253,3 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
cmd.Options = append(cmd.Options, uploadFlags.options()...)
return cmd
}
type createValidTemplateVersionArgs struct {
Name string
Message string
Client *codersdk.Client
Organization codersdk.Organization
Provisioner codersdk.ProvisionerType
FileID uuid.UUID
// Template is only required if updating a template's active version.
Template *codersdk.Template
// ReuseParameters will attempt to reuse params from the Template field
// before prompting the user. Set to false to always prompt for param
// values.
ReuseParameters bool
ProvisionerTags map[string]string
UserVariableValues []codersdk.VariableValue
}
func createValidTemplateVersion(inv *clibase.Invocation, args createValidTemplateVersionArgs) (*codersdk.TemplateVersion, error) {
client := args.Client
req := codersdk.CreateTemplateVersionRequest{
Name: args.Name,
Message: args.Message,
StorageMethod: codersdk.ProvisionerStorageMethodFile,
FileID: args.FileID,
Provisioner: args.Provisioner,
ProvisionerTags: args.ProvisionerTags,
UserVariableValues: args.UserVariableValues,
}
if args.Template != nil {
req.TemplateID = args.Template.ID
}
version, err := client.CreateTemplateVersion(inv.Context(), args.Organization.ID, req)
if err != nil {
return nil, err
}
err = cliui.ProvisionerJob(inv.Context(), inv.Stdout, cliui.ProvisionerJobOptions{
Fetch: func() (codersdk.ProvisionerJob, error) {
version, err := client.TemplateVersion(inv.Context(), version.ID)
return version.Job, err
},
Cancel: func() error {
return client.CancelTemplateVersion(inv.Context(), version.ID)
},
Logs: func() (<-chan codersdk.ProvisionerJobLog, io.Closer, error) {
return client.TemplateVersionLogsAfter(inv.Context(), version.ID, 0)
},
})
if err != nil {
var jobErr *cliui.ProvisionerJobError
if errors.As(err, &jobErr) && !codersdk.JobIsMissingParameterErrorCode(jobErr.Code) {
return nil, err
}
if err != nil {
return nil, err
}
}
version, err = client.TemplateVersion(inv.Context(), version.ID)
if err != nil {
return nil, err
}
if version.Job.Status != codersdk.ProvisionerJobSucceeded {
return nil, xerrors.New(version.Job.Error)
}
resources, err := client.TemplateVersionResources(inv.Context(), version.ID)
if err != nil {
return nil, err
}
// Only display the resources on the start transition, to avoid listing them more than once.
var startResources []codersdk.WorkspaceResource
for _, r := range resources {
if r.Transition == codersdk.WorkspaceTransitionStart {
startResources = append(startResources, r)
}
}
err = cliui.WorkspaceResources(inv.Stdout, startResources, cliui.WorkspaceResourcesOptions{
HideAgentState: true,
HideAccess: true,
Title: "Template Preview",
})
if err != nil {
return nil, xerrors.Errorf("preview template resources: %w", err)
}
return &version, nil
}
func ParseProvisionerTags(rawTags []string) (map[string]string, error) {
tags := map[string]string{}
for _, rawTag := range rawTags {
parts := strings.SplitN(rawTag, "=", 2)
if len(parts) < 2 {
return nil, xerrors.Errorf("invalid tag format for %q. must be key=value", rawTag)
}
tags[parts[0]] = parts[1]
}
return tags, nil
}

View File

@ -19,54 +19,6 @@ import (
"github.com/coder/coder/v2/testutil"
)
func completeWithAgent() *echo.Responses {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Resources: []*proto.Resource{
{
Type: "compute",
Name: "main",
Agents: []*proto.Agent{
{
Name: "smith",
OperatingSystem: "linux",
Architecture: "i386",
},
},
},
},
},
},
},
},
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{
{
Type: "compute",
Name: "main",
Agents: []*proto.Agent{
{
Name: "smith",
OperatingSystem: "linux",
Architecture: "i386",
},
},
},
},
},
},
},
},
}
}
func TestTemplateCreate(t *testing.T) {
t.Parallel()
t.Run("Create", func(t *testing.T) {
@ -418,15 +370,3 @@ func TestTemplateCreate(t *testing.T) {
require.Contains(t, err.Error(), "your deployment appears to be an AGPL deployment, so you cannot set enterprise-only flags")
})
}
// Need this for Windows because of a known issue with Go:
// https://github.com/golang/go/issues/52986
func removeTmpDirUntilSuccessAfterTest(t *testing.T, tempDir string) {
t.Helper()
t.Cleanup(func() {
err := os.RemoveAll(tempDir)
for err != nil {
err = os.RemoveAll(tempDir)
}
})
}

View File

@ -35,6 +35,7 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
allowUserAutostop bool
requireActiveVersion bool
deprecationMessage string
disableEveryone bool
)
client := new(codersdk.Client)
@ -162,6 +163,7 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
AllowUserAutostop: allowUserAutostop,
RequireActiveVersion: requireActiveVersion,
DeprecationMessage: deprecated,
DisableEveryoneGroupAccess: disableEveryone,
}
_, err = client.UpdateTemplateMeta(inv.Context(), template.ID, req)
@ -292,6 +294,13 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
Value: clibase.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: clibase.BoolOf(&disableEveryone),
Default: "false",
},
cliui.SkipPromptOption(),
}

View File

@ -113,7 +113,7 @@ func (*RootCmd) templateInit() *clibase.Cmd {
inv.Stdout,
pretty.Sprint(
cliui.DefaultStyles.Code,
"cd "+relPath+" && coder templates create"),
"cd "+relPath+" && coder templates push"),
)
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Wrap, "\nExamples provide a starting point and are expected to be edited! 🎨"))
return nil

View File

@ -36,7 +36,7 @@ func (r *RootCmd) templateList() *clibase.Cmd {
if len(templates) == 0 {
_, _ = fmt.Fprintf(inv.Stderr, "%s No templates found in %s! Create one:\n\n", Caret, color.HiWhiteString(organization.Name))
_, _ = fmt.Fprintln(inv.Stderr, color.HiMagentaString(" $ coder templates create <directory>\n"))
_, _ = fmt.Fprintln(inv.Stderr, color.HiMagentaString(" $ coder templates push <directory>\n"))
return nil
}

View File

@ -2,25 +2,210 @@ package cli
import (
"bufio"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"unicode/utf8"
"github.com/briandowns/spinner"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisionersdk"
"github.com/coder/pretty"
)
// templateUploadFlags is shared by `templates create` and `templates push`.
func (r *RootCmd) templatePush() *clibase.Cmd {
var (
versionName string
provisioner string
workdir string
variablesFile string
commandLineVariables []string
alwaysPrompt bool
provisionerTags []string
uploadFlags templateUploadFlags
activate bool
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
Use: "push [template]",
Short: "Create or update a template from the current directory or as specified by flag",
Middleware: clibase.Chain(
clibase.RequireRangeArgs(0, 1),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
uploadFlags.setWorkdir(workdir)
organization, err := CurrentOrganization(inv, client)
if err != nil {
return err
}
name, err := uploadFlags.templateName(inv.Args)
if err != nil {
return err
}
if utf8.RuneCountInString(name) >= 32 {
return xerrors.Errorf("Template name must be less than 32 characters")
}
var createTemplate bool
template, err := client.TemplateByName(inv.Context(), organization.ID, name)
if err != nil {
var apiError *codersdk.Error
if errors.As(err, &apiError) && apiError.StatusCode() != http.StatusNotFound {
return err
}
// Template doesn't exist, create it.
createTemplate = true
}
err = uploadFlags.checkForLockfile(inv)
if err != nil {
return xerrors.Errorf("check for lockfile: %w", err)
}
message := uploadFlags.templateMessage(inv)
resp, err := uploadFlags.upload(inv, client)
if err != nil {
return err
}
tags, err := ParseProvisionerTags(provisionerTags)
if err != nil {
return err
}
userVariableValues, err := ParseUserVariableValues(
variablesFile,
commandLineVariables)
if err != nil {
return err
}
args := createValidTemplateVersionArgs{
Message: message,
Client: client,
Organization: organization,
Provisioner: codersdk.ProvisionerType(provisioner),
FileID: resp.ID,
ProvisionerTags: tags,
UserVariableValues: userVariableValues,
}
if !createTemplate {
args.Name = versionName
args.Template = &template
args.ReuseParameters = !alwaysPrompt
}
job, err := createValidTemplateVersion(inv, args)
if err != nil {
return err
}
if job.Job.Status != codersdk.ProvisionerJobSucceeded {
return xerrors.Errorf("job failed: %s", job.Job.Status)
}
if createTemplate {
_, err = client.CreateTemplate(inv.Context(), organization.ID, codersdk.CreateTemplateRequest{
Name: name,
VersionID: job.ID,
})
if err != nil {
return err
}
_, _ = fmt.Fprintln(
inv.Stdout, "\n"+cliui.Wrap(
"The "+cliui.Keyword(name)+" template has been created at "+cliui.Timestamp(time.Now())+"! "+
"Developers can provision a workspace with this template using:")+"\n")
} else if activate {
err = client.UpdateActiveTemplateVersion(inv.Context(), template.ID, codersdk.UpdateActiveTemplateVersion{
ID: job.ID,
})
if err != nil {
return err
}
}
_, _ = fmt.Fprintf(inv.Stdout, "Updated version at %s!\n", pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp)))
return nil
},
}
cmd.Options = clibase.OptionSet{
{
Flag: "test.provisioner",
Description: "Customize the provisioner backend.",
Default: "terraform",
Value: clibase.StringOf(&provisioner),
// This is for testing!
Hidden: true,
},
{
Flag: "test.workdir",
Description: "Customize the working directory.",
Default: "",
Value: clibase.StringOf(&workdir),
// This is for testing!
Hidden: true,
},
{
Flag: "variables-file",
Description: "Specify a file path with values for Terraform-managed variables.",
Value: clibase.StringOf(&variablesFile),
},
{
Flag: "variable",
Description: "Specify a set of values for Terraform-managed variables.",
Value: clibase.StringArrayOf(&commandLineVariables),
},
{
Flag: "var",
Description: "Alias of --variable.",
Value: clibase.StringArrayOf(&commandLineVariables),
},
{
Flag: "provisioner-tag",
Description: "Specify a set of tags to target provisioner daemons.",
Value: clibase.StringArrayOf(&provisionerTags),
},
{
Flag: "name",
Description: "Specify a name for the new template version. It will be automatically generated if not provided.",
Value: clibase.StringOf(&versionName),
},
{
Flag: "always-prompt",
Description: "Always prompt all parameters. Does not pull parameter values from active template version.",
Value: clibase.BoolOf(&alwaysPrompt),
},
{
Flag: "activate",
Description: "Whether the new template will be marked active.",
Default: "true",
Value: clibase.BoolOf(&activate),
},
cliui.SkipPromptOption(),
}
cmd.Options = append(cmd.Options, uploadFlags.options()...)
return cmd
}
type templateUploadFlags struct {
directory string
ignoreLockfile bool
@ -154,188 +339,108 @@ func (pf *templateUploadFlags) templateName(args []string) (string, error) {
return filepath.Base(absPath), nil
}
func (r *RootCmd) templatePush() *clibase.Cmd {
var (
versionName string
provisioner string
workdir string
variablesFile string
commandLineVariables []string
alwaysPrompt bool
provisionerTags []string
uploadFlags templateUploadFlags
activate bool
create bool
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
Use: "push [template]",
Short: "Push a new template version from the current directory or as specified by flag",
Middleware: clibase.Chain(
clibase.RequireRangeArgs(0, 1),
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
uploadFlags.setWorkdir(workdir)
type createValidTemplateVersionArgs struct {
Name string
Message string
Client *codersdk.Client
Organization codersdk.Organization
Provisioner codersdk.ProvisionerType
FileID uuid.UUID
organization, err := CurrentOrganization(inv, client)
if err != nil {
return err
}
// Template is only required if updating a template's active version.
Template *codersdk.Template
// ReuseParameters will attempt to reuse params from the Template field
// before prompting the user. Set to false to always prompt for param
// values.
ReuseParameters bool
ProvisionerTags map[string]string
UserVariableValues []codersdk.VariableValue
}
name, err := uploadFlags.templateName(inv.Args)
if err != nil {
return err
}
func createValidTemplateVersion(inv *clibase.Invocation, args createValidTemplateVersionArgs) (*codersdk.TemplateVersion, error) {
client := args.Client
var createTemplate bool
template, err := client.TemplateByName(inv.Context(), organization.ID, name)
if err != nil {
if !create {
return err
}
createTemplate = true
}
err = uploadFlags.checkForLockfile(inv)
if err != nil {
return xerrors.Errorf("check for lockfile: %w", err)
}
message := uploadFlags.templateMessage(inv)
resp, err := uploadFlags.upload(inv, client)
if err != nil {
return err
}
tags, err := ParseProvisionerTags(provisionerTags)
if err != nil {
return err
}
userVariableValues, err := ParseUserVariableValues(
variablesFile,
commandLineVariables)
if err != nil {
return err
}
args := createValidTemplateVersionArgs{
Message: message,
Client: client,
Organization: organization,
Provisioner: codersdk.ProvisionerType(provisioner),
FileID: resp.ID,
ProvisionerTags: tags,
UserVariableValues: userVariableValues,
}
if !createTemplate {
args.Name = versionName
args.Template = &template
args.ReuseParameters = !alwaysPrompt
}
job, err := createValidTemplateVersion(inv, args)
if err != nil {
return err
}
if job.Job.Status != codersdk.ProvisionerJobSucceeded {
return xerrors.Errorf("job failed: %s", job.Job.Status)
}
if createTemplate {
_, err = client.CreateTemplate(inv.Context(), organization.ID, codersdk.CreateTemplateRequest{
Name: name,
VersionID: job.ID,
})
if err != nil {
return err
}
_, _ = fmt.Fprintln(
inv.Stdout, "\n"+cliui.Wrap(
"The "+cliui.Keyword(name)+" template has been created at "+cliui.Timestamp(time.Now())+"! "+
"Developers can provision a workspace with this template using:")+"\n")
} else if activate {
err = client.UpdateActiveTemplateVersion(inv.Context(), template.ID, codersdk.UpdateActiveTemplateVersion{
ID: job.ID,
})
if err != nil {
return err
}
}
_, _ = fmt.Fprintf(inv.Stdout, "Updated version at %s!\n", pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Now().Format(time.Stamp)))
return nil
},
req := codersdk.CreateTemplateVersionRequest{
Name: args.Name,
Message: args.Message,
StorageMethod: codersdk.ProvisionerStorageMethodFile,
FileID: args.FileID,
Provisioner: args.Provisioner,
ProvisionerTags: args.ProvisionerTags,
UserVariableValues: args.UserVariableValues,
}
if args.Template != nil {
req.TemplateID = args.Template.ID
}
version, err := client.CreateTemplateVersion(inv.Context(), args.Organization.ID, req)
if err != nil {
return nil, err
}
cmd.Options = clibase.OptionSet{
{
Flag: "test.provisioner",
Description: "Customize the provisioner backend.",
Default: "terraform",
Value: clibase.StringOf(&provisioner),
// This is for testing!
Hidden: true,
err = cliui.ProvisionerJob(inv.Context(), inv.Stdout, cliui.ProvisionerJobOptions{
Fetch: func() (codersdk.ProvisionerJob, error) {
version, err := client.TemplateVersion(inv.Context(), version.ID)
return version.Job, err
},
{
Flag: "test.workdir",
Description: "Customize the working directory.",
Default: "",
Value: clibase.StringOf(&workdir),
// This is for testing!
Hidden: true,
Cancel: func() error {
return client.CancelTemplateVersion(inv.Context(), version.ID)
},
{
Flag: "variables-file",
Description: "Specify a file path with values for Terraform-managed variables.",
Value: clibase.StringOf(&variablesFile),
Logs: func() (<-chan codersdk.ProvisionerJobLog, io.Closer, error) {
return client.TemplateVersionLogsAfter(inv.Context(), version.ID, 0)
},
{
Flag: "variable",
Description: "Specify a set of values for Terraform-managed variables.",
Value: clibase.StringArrayOf(&commandLineVariables),
},
{
Flag: "var",
Description: "Alias of --variable.",
Value: clibase.StringArrayOf(&commandLineVariables),
},
{
Flag: "provisioner-tag",
Description: "Specify a set of tags to target provisioner daemons.",
Value: clibase.StringArrayOf(&provisionerTags),
},
{
Flag: "name",
Description: "Specify a name for the new template version. It will be automatically generated if not provided.",
Value: clibase.StringOf(&versionName),
},
{
Flag: "always-prompt",
Description: "Always prompt all parameters. Does not pull parameter values from active template version.",
Value: clibase.BoolOf(&alwaysPrompt),
},
{
Flag: "activate",
Description: "Whether the new template will be marked active.",
Default: "true",
Value: clibase.BoolOf(&activate),
},
{
Flag: "create",
Description: "Create the template if it does not exist.",
Default: "false",
Value: clibase.BoolOf(&create),
},
cliui.SkipPromptOption(),
})
if err != nil {
var jobErr *cliui.ProvisionerJobError
if errors.As(err, &jobErr) && !codersdk.JobIsMissingParameterErrorCode(jobErr.Code) {
return nil, err
}
if err != nil {
return nil, err
}
}
cmd.Options = append(cmd.Options, uploadFlags.options()...)
return cmd
version, err = client.TemplateVersion(inv.Context(), version.ID)
if err != nil {
return nil, err
}
if version.Job.Status != codersdk.ProvisionerJobSucceeded {
return nil, xerrors.New(version.Job.Error)
}
resources, err := client.TemplateVersionResources(inv.Context(), version.ID)
if err != nil {
return nil, err
}
// Only display the resources on the start transition, to avoid listing them more than once.
var startResources []codersdk.WorkspaceResource
for _, r := range resources {
if r.Transition == codersdk.WorkspaceTransitionStart {
startResources = append(startResources, r)
}
}
err = cliui.WorkspaceResources(inv.Stdout, startResources, cliui.WorkspaceResourcesOptions{
HideAgentState: true,
HideAccess: true,
Title: "Template Preview",
})
if err != nil {
return nil, xerrors.Errorf("preview template resources: %w", err)
}
return &version, nil
}
func ParseProvisionerTags(rawTags []string) (map[string]string, error) {
tags := map[string]string{}
for _, rawTag := range rawTags {
parts := strings.SplitN(rawTag, "=", 2)
if len(parts) < 2 {
return nil, xerrors.Errorf("invalid tag format for %q. must be key=value", rawTag)
}
tags[parts[0]] = parts[1]
}
return tags, nil
}
// prettyDirectoryPath returns a prettified path when inside the users

View File

@ -679,7 +679,6 @@ func TestTemplatePush(t *testing.T) {
templateName,
"--directory", source,
"--test.provisioner", string(database.ProvisionerTypeEcho),
"--create",
}
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, templateAdmin, root)
@ -726,3 +725,63 @@ func createEchoResponsesWithTemplateVariables(templateVariables []*proto.Templat
ProvisionApply: echo.ApplyComplete,
}
}
func completeWithAgent() *echo.Responses {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
Resources: []*proto.Resource{
{
Type: "compute",
Name: "main",
Agents: []*proto.Agent{
{
Name: "smith",
OperatingSystem: "linux",
Architecture: "i386",
},
},
},
},
},
},
},
},
ProvisionApply: []*proto.Response{
{
Type: &proto.Response_Apply{
Apply: &proto.ApplyComplete{
Resources: []*proto.Resource{
{
Type: "compute",
Name: "main",
Agents: []*proto.Agent{
{
Name: "smith",
OperatingSystem: "linux",
Architecture: "i386",
},
},
},
},
},
},
},
},
}
}
// Need this for Windows because of a known issue with Go:
// https://github.com/golang/go/issues/52986
func removeTmpDirUntilSuccessAfterTest(t *testing.T, tempDir string) {
t.Helper()
t.Cleanup(func() {
err := os.RemoveAll(tempDir)
for err != nil {
err = os.RemoveAll(tempDir)
}
})
}

View File

@ -17,16 +17,12 @@ func (r *RootCmd) templates() *clibase.Cmd {
Use: "templates",
Short: "Manage templates",
Long: "Templates are written in standard Terraform and describe the infrastructure for workspaces\n" + formatExamples(
example{
Description: "Create a template for developers to create workspaces",
Command: "coder templates create",
},
example{
Description: "Make changes to your template, and plan the changes",
Command: "coder templates plan my-template",
},
example{
Description: "Push an update to the template. Your developers can update their workspaces",
Description: "Create or push an update to the template. Your developers can update their workspaces",
Command: "coder templates push my-template",
},
),

View File

@ -9,15 +9,11 @@ USAGE:
Templates are written in standard Terraform and describe the infrastructure
for workspaces
- Create a template for developers to create workspaces:
$ coder templates create
- Make changes to your template, and plan the changes:
$ coder templates plan my-template
- Push an update to the template. Your developers can update their
- Create or push an update to the template. Your developers can update their
workspaces:
$ coder templates push my-template
@ -25,15 +21,15 @@ USAGE:
SUBCOMMANDS:
archive Archive unused or failed template versions from a given
template(s)
create Create a template from the current directory or as specified by
flag
create DEPRECATED: Create a template from the current directory or as
specified by flag
delete Delete templates
edit Edit the metadata of a template by name.
init Get started with a templated template.
list List all the templates available for the organization
pull Download the active, latest, or specified version of a template
to a path.
push Push a new template version from the current directory or as
push Create or update a template from the current directory or as
specified by flag
versions Manage different versions of the specified template

View File

@ -3,7 +3,8 @@ coder v0.0.0-devel
USAGE:
coder templates create [flags] [name]
Create a template from the current directory or as specified by flag
DEPRECATED: Create a template from the current directory or as specified by
flag
OPTIONS:
--default-ttl duration (default: 24h)

View File

@ -66,6 +66,11 @@ OPTIONS:
--name string
Edit the template name.
--private bool (default: false)
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.
--require-active-version bool (default: false)
Requires workspace builds to use the active template version. This
setting does not apply to template admins. This is an enterprise-only

View File

@ -3,7 +3,7 @@ coder v0.0.0-devel
USAGE:
coder templates push [flags] [template]
Push a new template version from the current directory or as specified by flag
Create or update a template from the current directory or as specified by flag
OPTIONS:
--activate bool (default: true)
@ -13,9 +13,6 @@ OPTIONS:
Always prompt all parameters. Does not pull parameter values from
active template version.
--create bool (default: false)
Create the template if it does not exist.
-d, --directory string (default: .)
Specify the directory to create from, use '-' to read tar from stdin.

View File

@ -6373,6 +6373,7 @@ func (q *FakeQuerier) UpdateTemplateMetaByID(_ context.Context, arg database.Upd
tpl.DisplayName = arg.DisplayName
tpl.Description = arg.Description
tpl.Icon = arg.Icon
tpl.GroupACL = arg.GroupACL
q.templates[idx] = tpl
return nil
}

View File

@ -6075,19 +6075,21 @@ SET
name = $4,
icon = $5,
display_name = $6,
allow_user_cancel_workspace_jobs = $7
allow_user_cancel_workspace_jobs = $7,
group_acl = $8
WHERE
id = $1
`
type UpdateTemplateMetaByIDParams struct {
ID uuid.UUID `db:"id" json:"id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Description string `db:"description" json:"description"`
Name string `db:"name" json:"name"`
Icon string `db:"icon" json:"icon"`
DisplayName string `db:"display_name" json:"display_name"`
AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"`
ID uuid.UUID `db:"id" json:"id"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Description string `db:"description" json:"description"`
Name string `db:"name" json:"name"`
Icon string `db:"icon" json:"icon"`
DisplayName string `db:"display_name" json:"display_name"`
AllowUserCancelWorkspaceJobs bool `db:"allow_user_cancel_workspace_jobs" json:"allow_user_cancel_workspace_jobs"`
GroupACL TemplateACL `db:"group_acl" json:"group_acl"`
}
func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTemplateMetaByIDParams) error {
@ -6099,6 +6101,7 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl
arg.Icon,
arg.DisplayName,
arg.AllowUserCancelWorkspaceJobs,
arg.GroupACL,
)
return err
}

View File

@ -115,7 +115,8 @@ SET
name = $4,
icon = $5,
display_name = $6,
allow_user_cancel_workspace_jobs = $7
allow_user_cancel_workspace_jobs = $7,
group_acl = $8
WHERE
id = $1
;

View File

@ -667,6 +667,11 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
name = template.Name
}
groupACL := template.GroupACL
if req.DisableEveryoneGroupAccess {
groupACL = database.TemplateACL{}
}
var err error
err = tx.UpdateTemplateMetaByID(ctx, database.UpdateTemplateMetaByIDParams{
ID: template.ID,
@ -676,6 +681,7 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
Description: req.Description,
Icon: req.Icon,
AllowUserCancelWorkspaceJobs: req.AllowUserCancelWorkspaceJobs,
GroupACL: groupACL,
})
if err != nil {
return xerrors.Errorf("update template metadata: %w", err)

View File

@ -241,6 +241,12 @@ type UpdateTemplateMeta struct {
// If passed an empty string, will remove the deprecated message, making
// the template usable for new workspaces again.
DeprecationMessage *string `json:"deprecation_message"`
// DisableEveryoneGroupAccess allows optionally disabling the default
// behavior of granting the 'everyone' group access to use the template.
// If this is set to true, the template will not be available to all users,
// and must be explicitly granted to users or groups in the permissions settings
// of the template.
DisableEveryoneGroupAccess bool `json:"disable_everyone_group_access"`
}
type TemplateExample struct {

View File

@ -64,11 +64,11 @@ the [Helm example](#example-running-an-external-provisioner-with-helm) below.
# In another terminal, create/push
# a template that requires this provisioner
coder templates create on-prem \
coder templates push on-prem \
--provisioner-tag environment=on_prem
# Or, match the provisioner exactly
coder templates create on-prem-chicago \
coder templates push on-prem-chicago \
--provisioner-tag environment=on_prem \
--provisioner-tag data_center=chicago
```
@ -88,7 +88,7 @@ the [Helm example](#example-running-an-external-provisioner-with-helm) below.
# In another terminal, create/push
# a template that requires user provisioners
coder templates create on-prem \
coder templates push on-prem \
--provisioner-tag scope=user
```

29
docs/cli/templates.md generated
View File

@ -18,29 +18,26 @@ coder templates
```console
Templates are written in standard Terraform and describe the infrastructure for workspaces
- Create a template for developers to create workspaces:
$ coder templates create
- Make changes to your template, and plan the changes:
$ coder templates plan my-template
- Push an update to the template. Your developers can update their workspaces:
- Create or push an update to the template. Your developers can update their
workspaces:
$ coder templates push my-template
```
## Subcommands
| Name | Purpose |
| ------------------------------------------------ | ------------------------------------------------------------------------------ |
| [<code>archive</code>](./templates_archive.md) | Archive unused or failed template versions from a given template(s) |
| [<code>create</code>](./templates_create.md) | Create a template from the current directory or as specified by flag |
| [<code>delete</code>](./templates_delete.md) | Delete templates |
| [<code>edit</code>](./templates_edit.md) | Edit the metadata of a template by name. |
| [<code>init</code>](./templates_init.md) | Get started with a templated template. |
| [<code>list</code>](./templates_list.md) | List all the templates available for the organization |
| [<code>pull</code>](./templates_pull.md) | Download the active, latest, or specified version of a template to a path. |
| [<code>push</code>](./templates_push.md) | Push a new template version from the current directory or as specified by flag |
| [<code>versions</code>](./templates_versions.md) | Manage different versions of the specified template |
| Name | Purpose |
| ------------------------------------------------ | -------------------------------------------------------------------------------- |
| [<code>archive</code>](./templates_archive.md) | Archive unused or failed template versions from a given template(s) |
| [<code>create</code>](./templates_create.md) | DEPRECATED: Create a template from the current directory or as specified by flag |
| [<code>delete</code>](./templates_delete.md) | Delete templates |
| [<code>edit</code>](./templates_edit.md) | Edit the metadata of a template by name. |
| [<code>init</code>](./templates_init.md) | Get started with a templated template. |
| [<code>list</code>](./templates_list.md) | List all the templates available for the organization |
| [<code>pull</code>](./templates_pull.md) | Download the active, latest, or specified version of a template to a path. |
| [<code>push</code>](./templates_push.md) | Create or update a template from the current directory or as specified by flag |
| [<code>versions</code>](./templates_versions.md) | Manage different versions of the specified template |

View File

@ -2,7 +2,7 @@
# templates create
Create a template from the current directory or as specified by flag
DEPRECATED: Create a template from the current directory or as specified by flag
## Usage

View File

@ -130,6 +130,15 @@ Edit the template maximum time before shutdown - workspaces created from this te
Edit the template name.
### --private
| | |
| ------- | ------------------ |
| Type | <code>bool</code> |
| Default | <code>false</code> |
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.
### --require-active-version
| | |

View File

@ -2,7 +2,7 @@
# templates push
Push a new template version from the current directory or as specified by flag
Create or update a template from the current directory or as specified by flag
## Usage
@ -29,15 +29,6 @@ Whether the new template will be marked active.
Always prompt all parameters. Does not pull parameter values from active template version.
### --create
| | |
| ------- | ------------------ |
| Type | <code>bool</code> |
| Default | <code>false</code> |
Create the template if it does not exist.
### -d, --directory
| | |

View File

@ -322,7 +322,7 @@ Edit `main.tf` and update the following fields of the Kubernetes pod resource:
Finally, create the template:
```console
coder template create kubernetes -d .
coder template push kubernetes -d .
```
This template should be ready to use straight away.

View File

@ -892,7 +892,7 @@
},
{
"title": "templates create",
"description": "Create a template from the current directory or as specified by flag",
"description": "DEPRECATED: Create a template from the current directory or as specified by flag",
"path": "cli/templates_create.md"
},
{
@ -922,7 +922,7 @@
},
{
"title": "templates push",
"description": "Push a new template version from the current directory or as specified by flag",
"description": "Create or update a template from the current directory or as specified by flag",
"path": "cli/templates_push.md"
},
{

View File

@ -128,7 +128,7 @@ Navigate to the `./azure-linux` folder where you created your template and run
the following command to put the template on your Coder instance.
```shell
coder templates create
coder templates push
```
Congrats! You can now navigate to your Coder dashboard and use this Linux on

View File

@ -52,7 +52,7 @@ Coder with Docker has the following advantages:
cd docker
```
1. Push up the template with `coder templates create`
1. Push up the template with `coder templates push`
1. Open the dashboard in your browser to create your first workspace:

View File

@ -211,7 +211,7 @@ export CLUSTER_SERVICEACCOUNT_TOKEN=$(kubectl get secrets coder-v2 -n coder-work
Create the template with these values:
```shell
coder templates create \
coder templates push \
--variable host=$CLUSTER_ADDRESS \
--variable cluster_ca_certificate=$CLUSTER_CA_CERTIFICATE \
--variable token=$CLUSTER_SERVICEACCOUNT_TOKEN \
@ -228,7 +228,7 @@ kubectl cluster-info
# Get cluster CA and token (base64 encoded)
kubectl get secrets coder-service-account-token -n coder-workspaces -o jsonpath="{.data}"
coder templates create \
coder templates push \
--variable host=API_ADDRESS \
--variable cluster_ca_certificate=CLUSTER_CA_CERTIFICATE \
--variable token=CLUSTER_SERVICEACCOUNT_TOKEN \

View File

@ -808,6 +808,39 @@ func TestTemplateACL(t *testing.T) {
require.Equal(t, http.StatusNotFound, cerr.StatusCode())
})
t.Run("DisableEveryoneGroupAccess", func(t *testing.T) {
t.Parallel()
client, admin := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureTemplateRBAC: 1,
},
}})
version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
//nolint:gocritic // non-template-admin cannot get template acl
acl, err := client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
require.Equal(t, 1, len(acl.Groups))
_, err = client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
Name: template.Name,
DisplayName: template.DisplayName,
Description: template.Description,
Icon: template.Icon,
AllowUserCancelWorkspaceJobs: template.AllowUserCancelWorkspaceJobs,
DisableEveryoneGroupAccess: true,
})
require.NoError(t, err)
acl, err = client.TemplateACL(ctx, template.ID)
require.NoError(t, err)
require.Equal(t, 0, len(acl.Groups), acl.Groups)
})
// Test that we do not return deleted users.
t.Run("FilterDeletedUsers", func(t *testing.T) {
t.Parallel()

View File

@ -155,6 +155,6 @@
"nomad",
"container"
],
"markdown": "\n# Remote Development on Nomad\n\nProvision Nomad Jobs as [Coder workspaces](https://coder.com/docs/coder-v2/latest) with this example template. This example shows how to use Nomad service tasks to be used as a development environment using docker and host csi volumes.\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## Prerequisites\n\n- [Nomad](https://www.nomadproject.io/downloads)\n- [Docker](https://docs.docker.com/get-docker/)\n\n## Setup\n\n### 1. Start the CSI Host Volume Plugin\n\nThe CSI Host Volume plugin is used to mount host volumes into Nomad tasks. This is useful for development environments where you want to mount persistent volumes into your container workspace.\n\n1. Login to the Nomad server using SSH.\n\n2. Append the following stanza to your Nomad server configuration file and restart the nomad service.\n\n ```hcl\n plugin \"docker\" {\n config {\n allow_privileged = true\n }\n }\n ```\n\n ```shell\n sudo systemctl restart nomad\n ```\n\n3. Create a file `hostpath.nomad` with following content:\n\n ```hcl\n job \"hostpath-csi-plugin\" {\n datacenters = [\"dc1\"]\n type = \"system\"\n\n group \"csi\" {\n task \"plugin\" {\n driver = \"docker\"\n\n config {\n image = \"registry.k8s.io/sig-storage/hostpathplugin:v1.10.0\"\n\n args = [\n \"--drivername=csi-hostpath\",\n \"--v=5\",\n \"--endpoint=${CSI_ENDPOINT}\",\n \"--nodeid=node-${NOMAD_ALLOC_INDEX}\",\n ]\n\n privileged = true\n }\n\n csi_plugin {\n id = \"hostpath\"\n type = \"monolith\"\n mount_dir = \"/csi\"\n }\n\n resources {\n cpu = 256\n memory = 128\n }\n }\n }\n }\n ```\n\n4. Run the job:\n\n ```shell\n nomad job run hostpath.nomad\n ```\n\n### 2. Setup the Nomad Template\n\n1. Create the template by running the following command:\n\n ```shell\n coder template init nomad-docker\n cd nomad-docker\n coder template create\n ```\n\n2. Set up Nomad server address and optional authentication:\n\n3. Create a new workspace and start developing.\n"
"markdown": "\n# Remote Development on Nomad\n\nProvision Nomad Jobs as [Coder workspaces](https://coder.com/docs/coder-v2/latest) with this example template. This example shows how to use Nomad service tasks to be used as a development environment using docker and host csi volumes.\n\n\u003c!-- TODO: Add screenshot --\u003e\n\n\u003e **Note**\n\u003e This template is designed to be a starting point! Edit the Terraform to extend the template to support your use case.\n\n## Prerequisites\n\n- [Nomad](https://www.nomadproject.io/downloads)\n- [Docker](https://docs.docker.com/get-docker/)\n\n## Setup\n\n### 1. Start the CSI Host Volume Plugin\n\nThe CSI Host Volume plugin is used to mount host volumes into Nomad tasks. This is useful for development environments where you want to mount persistent volumes into your container workspace.\n\n1. Login to the Nomad server using SSH.\n\n2. Append the following stanza to your Nomad server configuration file and restart the nomad service.\n\n ```hcl\n plugin \"docker\" {\n config {\n allow_privileged = true\n }\n }\n ```\n\n ```shell\n sudo systemctl restart nomad\n ```\n\n3. Create a file `hostpath.nomad` with following content:\n\n ```hcl\n job \"hostpath-csi-plugin\" {\n datacenters = [\"dc1\"]\n type = \"system\"\n\n group \"csi\" {\n task \"plugin\" {\n driver = \"docker\"\n\n config {\n image = \"registry.k8s.io/sig-storage/hostpathplugin:v1.10.0\"\n\n args = [\n \"--drivername=csi-hostpath\",\n \"--v=5\",\n \"--endpoint=${CSI_ENDPOINT}\",\n \"--nodeid=node-${NOMAD_ALLOC_INDEX}\",\n ]\n\n privileged = true\n }\n\n csi_plugin {\n id = \"hostpath\"\n type = \"monolith\"\n mount_dir = \"/csi\"\n }\n\n resources {\n cpu = 256\n memory = 128\n }\n }\n }\n }\n ```\n\n4. Run the job:\n\n ```shell\n nomad job run hostpath.nomad\n ```\n\n### 2. Setup the Nomad Template\n\n1. Create the template by running the following command:\n\n ```shell\n coder template init nomad-docker\n cd nomad-docker\n coder template push\n ```\n\n2. Set up Nomad server address and optional authentication:\n\n3. Create a new workspace and start developing.\n"
}
]

View File

@ -103,7 +103,7 @@ provision:
fi
DOCKER_HOST=$(docker context inspect --format '{{.Endpoints.docker.Host}}')
printf 'docker_arch: "%s"\ndocker_host: "%s"\n' "${DOCKER_ARCH}" "${DOCKER_HOST}" | tee "${temp_template_dir}/params.yaml"
coder templates create "docker-${DOCKER_ARCH}" --directory "${temp_template_dir}" --variables-file "${temp_template_dir}/params.yaml" --yes
coder templates push "docker-${DOCKER_ARCH}" --directory "${temp_template_dir}" --variables-file "${temp_template_dir}/params.yaml" --yes
rm -rfv "${temp_template_dir}"
probes:
- description: "docker to be installed"

View File

@ -11,7 +11,7 @@ Clone this repository to create a template from any example listed here:
```console
git clone https://github.com/coder/coder
cd examples/templates/aws-linux
coder templates create
coder templates push
```
## Community Templates

View File

@ -47,7 +47,7 @@ To supply values to existing existing Terraform variables you can specify the
`-V` flag. For example
```bash
coder templates create envbox --var namespace="mynamespace" --var max_cpus=2 --var min_cpus=1 --var max_memory=4 --var min_memory=1
coder templates push envbox --var namespace="mynamespace" --var max_cpus=2 --var min_cpus=1 --var max_memory=4 --var min_memory=1
```
## Contributions

View File

@ -95,7 +95,7 @@ The CSI Host Volume plugin is used to mount host volumes into Nomad tasks. This
```shell
coder template init nomad-docker
cd nomad-docker
coder template create
coder template push
```
2. Set up Nomad server address and optional authentication:

View File

@ -68,7 +68,7 @@ CODER_FIRST_USER_TRIAL="${CODER_FIRST_USER_TRIAL}"
EOF
echo "Importing kubernetes template"
DRY_RUN="$DRY_RUN" "$PROJECT_ROOT/scaletest/lib/coder_shim.sh" templates create \
DRY_RUN="$DRY_RUN" "$PROJECT_ROOT/scaletest/lib/coder_shim.sh" templates push \
--global-config="${CONFIG_DIR}" \
--directory "${CONFIG_DIR}/templates/kubernetes" \
--yes kubernetes

View File

@ -177,7 +177,7 @@ fatal() {
DOCKER_HOST="$(docker context inspect --format '{{ .Endpoints.docker.Host }}')"
printf 'docker_arch: "%s"\ndocker_host: "%s"\n' "${GOARCH}" "${DOCKER_HOST}" >"${temp_template_dir}/params.yaml"
(
"${CODER_DEV_SHIM}" templates create "${template_name}" --directory "${temp_template_dir}" --variables-file "${temp_template_dir}/params.yaml" --yes
"${CODER_DEV_SHIM}" templates push "${template_name}" --directory "${temp_template_dir}" --variables-file "${temp_template_dir}/params.yaml" --yes
rm -rfv "${temp_template_dir}" # Only delete template dir if template creation succeeds
) || echo "Failed to create a template. The template files are in ${temp_template_dir}"
fi

View File

@ -1265,6 +1265,7 @@ export interface UpdateTemplateMeta {
readonly update_workspace_dormant_at: boolean;
readonly require_active_version: boolean;
readonly deprecation_message?: string;
readonly disable_everyone_group_access: boolean;
}
// From codersdk/users.go

View File

@ -77,6 +77,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
update_workspace_dormant_at: false,
require_active_version: template.require_active_version,
deprecation_message: template.deprecation_message,
disable_everyone_group_access: false,
},
validationSchema,
onSubmit,

View File

@ -47,6 +47,7 @@ const validFormValues: FormValues = {
update_workspace_last_used_at: false,
update_workspace_dormant_at: false,
require_active_version: false,
disable_everyone_group_access: false,
};
const renderTemplateSettingsPage = async () => {

View File

@ -118,6 +118,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
update_workspace_last_used_at: false,
update_workspace_dormant_at: false,
require_active_version: false,
disable_everyone_group_access: false,
},
validationSchema,
onSubmit: () => {
@ -238,6 +239,7 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
update_workspace_last_used_at: form.values.update_workspace_last_used_at,
update_workspace_dormant_at: form.values.update_workspace_dormant_at,
require_active_version: false,
disable_everyone_group_access: false,
});
};

View File

@ -37,6 +37,7 @@ const validFormValues: TemplateScheduleFormValues = {
"saturday",
"sunday",
],
disable_everyone_group_access: false,
};
const renderTemplateSchedulePage = async () => {