From 6509d1379f8c83132bf2b629e86edb430a832a1a Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 13 Apr 2023 11:25:49 +0100 Subject: [PATCH 1/2] feat(cli): add --id parameter to templates init command --- cli/templateinit.go | 78 +++++++++++++++---- cli/templateinit_test.go | 23 ++++++ .../coder_templates_init_--help.golden | 6 +- docs/cli/templates_init.md | 12 ++- examples/lima/coder.yaml | 2 +- scripts/develop.sh | 3 +- 6 files changed, 105 insertions(+), 19 deletions(-) diff --git a/cli/templateinit.go b/cli/templateinit.go index ff7a99eefbd3c..8d46049bb8b21 100644 --- a/cli/templateinit.go +++ b/cli/templateinit.go @@ -5,6 +5,9 @@ import ( "fmt" "os" "path/filepath" + "sort" + + "golang.org/x/xerrors" "github.com/coder/coder/cli/clibase" "github.com/coder/coder/cli/cliui" @@ -14,7 +17,8 @@ import ( ) func (*RootCmd) templateInit() *clibase.Cmd { - return &clibase.Cmd{ + var templateIDArg string + cmd := &clibase.Cmd{ Use: "init [directory]", Short: "Get started with a templated template.", Middleware: clibase.RequireRangeArgs(0, 1), @@ -23,8 +27,8 @@ func (*RootCmd) templateInit() *clibase.Cmd { if err != nil { return err } - exampleNames := []string{} - exampleByName := map[string]codersdk.TemplateExample{} + + optsToID := map[string]string{} for _, example := range exampleList { name := fmt.Sprintf( "%s\n%s\n%s\n", @@ -32,20 +36,31 @@ func (*RootCmd) templateInit() *clibase.Cmd { 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 + optsToID[name] = example.ID } - _, _ = 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 + // If the user didn't specify any template, prompt them to select one. + if templateIDArg == "" { + opts := 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: sort.StringSlice(keys(optsToID)), + }) + if err != nil { + return err + } + templateIDArg = optsToID[selected] + } + + selectedTemplate, ok := templateByID(templateIDArg, exampleList) + if !ok { + ids := values(optsToID) + sort.Strings(ids) + return xerrors.Errorf("Template ID %q does not exist!\nValid options are: %q", templateIDArg, ids) } - selectedTemplate := exampleByName[option] archive, err := examples.Archive(selectedTemplate.ID) if err != nil { return err @@ -81,4 +96,39 @@ func (*RootCmd) templateInit() *clibase.Cmd { return nil }, } + + cmd.Options = clibase.OptionSet{ + { + Flag: "id", + Description: "Specify a given example template by ID.", + Value: clibase.StringOf(&templateIDArg), + }, + } + + 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 +} + +func keys[K comparable, V any](m map[K]V) []K { + l := make([]K, 0, len(m)) + for k := range m { + l = append(l, k) + } + return l +} + +func values[K comparable, V any](m map[K]V) []V { + l := make([]V, 0, len(m)) + for _, v := range m { + l = append(l, v) + } + return l } diff --git a/cli/templateinit_test.go b/cli/templateinit_test.go index 9663d5ebd5776..78044525a4cfb 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, "does not exist!") + 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..824777aafbec8 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 string + 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..d0df50d986746 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 | string | + +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" From cccf0f1120d87c8342f544f0a1966e690ba7b7d6 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Thu, 13 Apr 2023 12:09:35 +0100 Subject: [PATCH 2/2] address PR comments --- cli/templateinit.go | 81 +++++++++---------- cli/templateinit_test.go | 2 +- .../coder_templates_init_--help.golden | 2 +- docs/cli/templates_init.md | 6 +- 4 files changed, 44 insertions(+), 47 deletions(-) diff --git a/cli/templateinit.go b/cli/templateinit.go index 8d46049bb8b21..82ec3f845b846 100644 --- a/cli/templateinit.go +++ b/cli/templateinit.go @@ -2,11 +2,14 @@ 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" @@ -17,49 +20,59 @@ import ( ) func (*RootCmd) templateInit() *clibase.Cmd { - var templateIDArg string + 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 - } - - 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 - } - // If the user didn't specify any template, prompt them to select one. - if templateIDArg == "" { - opts := keys(optsToID) + 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: sort.StringSlice(keys(optsToID)), + 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 } - templateIDArg = optsToID[selected] + templateID = optsToID[selected] } - selectedTemplate, ok := templateByID(templateIDArg, exampleList) + selectedTemplate, ok := templateByID(templateID, exampleList) if !ok { - ids := values(optsToID) - sort.Strings(ids) - return xerrors.Errorf("Template ID %q does not exist!\nValid options are: %q", templateIDArg, ids) + // clibase.EnumOf would normally handle this. + return xerrors.Errorf("template not found: %q", templateID) } archive, err := examples.Archive(selectedTemplate.ID) if err != nil { @@ -101,7 +114,7 @@ func (*RootCmd) templateInit() *clibase.Cmd { { Flag: "id", Description: "Specify a given example template by ID.", - Value: clibase.StringOf(&templateIDArg), + Value: clibase.EnumOf(&templateID, templateIDs...), }, } @@ -116,19 +129,3 @@ func templateByID(templateID string, tes []codersdk.TemplateExample) (codersdk.T } return codersdk.TemplateExample{}, false } - -func keys[K comparable, V any](m map[K]V) []K { - l := make([]K, 0, len(m)) - for k := range m { - l = append(l, k) - } - return l -} - -func values[K comparable, V any](m map[K]V) []V { - l := make([]V, 0, len(m)) - for _, v := range m { - l = append(l, v) - } - return l -} diff --git a/cli/templateinit_test.go b/cli/templateinit_test.go index 78044525a4cfb..ba99f76e95ece 100644 --- a/cli/templateinit_test.go +++ b/cli/templateinit_test.go @@ -40,7 +40,7 @@ func TestTemplateInit(t *testing.T) { inv, _ := clitest.New(t, "templates", "init", "--id", "thistemplatedoesnotexist", tempDir) ptytest.New(t).Attach(inv) err := inv.Run() - require.ErrorContains(t, err, "does not exist!") + 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 824777aafbec8..2a60cc6e0c2d8 100644 --- a/cli/testdata/coder_templates_init_--help.golden +++ b/cli/testdata/coder_templates_init_--help.golden @@ -3,7 +3,7 @@ Usage: coder templates init [flags] [directory] Get started with a templated template. Options - --id string + --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. --- diff --git a/docs/cli/templates_init.md b/docs/cli/templates_init.md index d0df50d986746..a128d3533c189 100644 --- a/docs/cli/templates_init.md +++ b/docs/cli/templates_init.md @@ -14,8 +14,8 @@ coder templates init [flags] [directory] ### --id -| | | -| ---- | ------------------- | -| Type | string | +| | | +| ---- | ---------------------------- | --------- | ----------- | ----------- | -------- | ------ | -------------------- | ---------------- | --------- | ---------------- | ----------- | ------------------ | +| 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.