diff --git a/cli/templateinit.go b/cli/templateinit.go index ff7a99eefbd3c..82ec3f845b846 100644 --- a/cli/templateinit.go +++ b/cli/templateinit.go @@ -2,9 +2,15 @@ package cli import ( "bytes" + "errors" "fmt" + "io" "os" "path/filepath" + "sort" + + "golang.org/x/exp/maps" + "golang.org/x/xerrors" "github.com/coder/coder/cli/clibase" "github.com/coder/coder/cli/cliui" @@ -14,38 +20,60 @@ import ( ) func (*RootCmd) templateInit() *clibase.Cmd { - return &clibase.Cmd{ + var templateID string + exampleList, err := examples.List() + if err != nil { + // This should not happen. If it does, something is very wrong. + panic(err) + } + var templateIDs []string + for _, ex := range exampleList { + templateIDs = append(templateIDs, ex.ID) + } + sort.Strings(templateIDs) + cmd := &clibase.Cmd{ Use: "init [directory]", Short: "Get started with a templated template.", Middleware: clibase.RequireRangeArgs(0, 1), Handler: func(inv *clibase.Invocation) error { - exampleList, err := examples.List() - if err != nil { - return err - } - exampleNames := []string{} - exampleByName := map[string]codersdk.TemplateExample{} - for _, example := range exampleList { - name := fmt.Sprintf( - "%s\n%s\n%s\n", - cliui.Styles.Bold.Render(example.Name), - cliui.Styles.Wrap.Copy().PaddingLeft(6).Render(example.Description), - cliui.Styles.Keyword.Copy().PaddingLeft(6).Render(example.URL), - ) - exampleNames = append(exampleNames, name) - exampleByName[name] = example + // If the user didn't specify any template, prompt them to select one. + if templateID == "" { + optsToID := map[string]string{} + for _, example := range exampleList { + name := fmt.Sprintf( + "%s\n%s\n%s\n", + cliui.Styles.Bold.Render(example.Name), + cliui.Styles.Wrap.Copy().PaddingLeft(6).Render(example.Description), + cliui.Styles.Keyword.Copy().PaddingLeft(6).Render(example.URL), + ) + optsToID[name] = example.ID + } + opts := maps.Keys(optsToID) + sort.Strings(opts) + _, _ = fmt.Fprintln(inv.Stdout, cliui.Styles.Wrap.Render( + "A template defines infrastructure as code to be provisioned "+ + "for individual developer workspaces. Select an example to be copied to the active directory:\n")) + selected, err := cliui.Select(inv, cliui.SelectOptions{ + Options: opts, + }) + if err != nil { + if errors.Is(err, io.EOF) { + return xerrors.Errorf( + "Couldn't find a matching template!\n" + + "Tip: if you're trying to automate template creation, try\n" + + "coder templates init --id instead!", + ) + } + return err + } + templateID = optsToID[selected] } - _, _ = fmt.Fprintln(inv.Stdout, cliui.Styles.Wrap.Render( - "A template defines infrastructure as code to be provisioned "+ - "for individual developer workspaces. Select an example to be copied to the active directory:\n")) - option, err := cliui.Select(inv, cliui.SelectOptions{ - Options: exampleNames, - }) - if err != nil { - return err + selectedTemplate, ok := templateByID(templateID, exampleList) + if !ok { + // clibase.EnumOf would normally handle this. + return xerrors.Errorf("template not found: %q", templateID) } - selectedTemplate := exampleByName[option] archive, err := examples.Archive(selectedTemplate.ID) if err != nil { return err @@ -81,4 +109,23 @@ func (*RootCmd) templateInit() *clibase.Cmd { return nil }, } + + cmd.Options = clibase.OptionSet{ + { + Flag: "id", + Description: "Specify a given example template by ID.", + Value: clibase.EnumOf(&templateID, templateIDs...), + }, + } + + return cmd +} + +func templateByID(templateID string, tes []codersdk.TemplateExample) (codersdk.TemplateExample, bool) { + for _, te := range tes { + if te.ID == templateID { + return te, true + } + } + return codersdk.TemplateExample{}, false } diff --git a/cli/templateinit_test.go b/cli/templateinit_test.go index 9663d5ebd5776..ba99f76e95ece 100644 --- a/cli/templateinit_test.go +++ b/cli/templateinit_test.go @@ -22,4 +22,27 @@ func TestTemplateInit(t *testing.T) { require.NoError(t, err) require.Greater(t, len(files), 0) }) + + t.Run("ExtractSpecific", func(t *testing.T) { + t.Parallel() + tempDir := t.TempDir() + inv, _ := clitest.New(t, "templates", "init", "--id", "docker", tempDir) + ptytest.New(t).Attach(inv) + clitest.Run(t, inv) + files, err := os.ReadDir(tempDir) + require.NoError(t, err) + require.Greater(t, len(files), 0) + }) + + t.Run("NotFound", func(t *testing.T) { + t.Parallel() + tempDir := t.TempDir() + inv, _ := clitest.New(t, "templates", "init", "--id", "thistemplatedoesnotexist", tempDir) + ptytest.New(t).Attach(inv) + err := inv.Run() + require.ErrorContains(t, err, "invalid choice: thistemplatedoesnotexist, should be one of") + files, err := os.ReadDir(tempDir) + require.NoError(t, err) + require.Empty(t, files) + }) } diff --git a/cli/testdata/coder_templates_init_--help.golden b/cli/testdata/coder_templates_init_--help.golden index acf84939b4f4e..2a60cc6e0c2d8 100644 --- a/cli/testdata/coder_templates_init_--help.golden +++ b/cli/testdata/coder_templates_init_--help.golden @@ -1,6 +1,10 @@ -Usage: coder templates init [directory] +Usage: coder templates init [flags] [directory] Get started with a templated template. +Options + --id aws-ecs-container|aws-linux|aws-windows|azure-linux|do-linux|docker|docker-with-dotfiles|fly-docker-image|gcp-linux|gcp-vm-container|gcp-windows|kubernetes + Specify a given example template by ID. + --- Run `coder --help` for a list of global options. diff --git a/docs/cli/templates_init.md b/docs/cli/templates_init.md index 722607a3ef022..a128d3533c189 100644 --- a/docs/cli/templates_init.md +++ b/docs/cli/templates_init.md @@ -7,5 +7,15 @@ Get started with a templated template. ## Usage ```console -coder templates init [directory] +coder templates init [flags] [directory] ``` + +## Options + +### --id + +| | | +| ---- | ---------------------------- | --------- | ----------- | ----------- | -------- | ------ | -------------------- | ---------------- | --------- | ---------------- | ----------- | ------------------ | +| Type | enum[aws-ecs-container | aws-linux | aws-windows | azure-linux | do-linux | docker | docker-with-dotfiles | fly-docker-image | gcp-linux | gcp-vm-container | gcp-windows | kubernetes] | + +Specify a given example template by ID. diff --git a/examples/lima/coder.yaml b/examples/lima/coder.yaml index 7b4382e19742f..7eb5566df327b 100644 --- a/examples/lima/coder.yaml +++ b/examples/lima/coder.yaml @@ -96,7 +96,7 @@ provision: [ ! -e ~/.config/coderv2/session ] && coder login http://localhost:3000 --first-user-username admin --first-user-email admin@coder.com --first-user-password $(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c12 | tee ${HOME}/.config/coderv2/password) # Create an initial template temp_template_dir=$(mktemp -d) - echo code-server | coder templates init "${temp_template_dir}" + coder templates init --id docker "${temp_template_dir}" DOCKER_ARCH="amd64" if [ "$(arch)" = "aarch64" ]; then DOCKER_ARCH="arm64" diff --git a/scripts/develop.sh b/scripts/develop.sh index 93696f36f16cc..eb4f6f4262d7d 100755 --- a/scripts/develop.sh +++ b/scripts/develop.sh @@ -151,7 +151,6 @@ fatal() { # If we have docker available and the "docker" template doesn't already # exist, then let's try to create a template! - example_template="code-server" template_name="docker" if docker info >/dev/null 2>&1 && ! "${CODER_DEV_SHIM}" templates versions list "${template_name}" >/dev/null 2>&1; then # sometimes terraform isn't installed yet when we go to create the @@ -159,7 +158,7 @@ fatal() { sleep 5 temp_template_dir="$(mktemp -d)" - echo "${example_template}" | "${CODER_DEV_SHIM}" templates init "${temp_template_dir}" + "${CODER_DEV_SHIM}" templates init --id "${template_name}" "${temp_template_dir}" 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"