Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(cli): add --id parameter to templates init command #7116

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 13, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 64 additions & 14 deletions cli/templateinit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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),
Expand All @@ -23,29 +27,40 @@ 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",
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
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a biggie, but this was a bit of a brain twister for me since the name/id stored in the map seemed unintuitive. 😅 To me it would make sense to have opts[id] = name.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our goal is to have an example template ID that we can pass to examples.Archive. The choices passed to cliui.Select are just literally what gets displayed, so we have to map that back to ID. I'd honestly prefer if cliui.Select took a map[string]string so you could differentiate what gets displayed to what gets returned, but that's a bigger change than I want to make here.

}
selectedTemplate := exampleByName[option]
archive, err := examples.Archive(selectedTemplate.ID)
if err != nil {
return err
Expand Down Expand Up @@ -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
}
23 changes: 23 additions & 0 deletions cli/templateinit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
6 changes: 5 additions & 1 deletion cli/testdata/coder_templates_init_--help.golden
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 11 additions & 1 deletion docs/cli/templates_init.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,15 @@ Get started with a templated template.
## Usage

```console
coder templates init [directory]
coder templates init [flags] [directory]
```

## Options

### --id

| | |
| ---- | ------------------- |
| Type | <code>string</code> |

Specify a given example template by ID.
2 changes: 1 addition & 1 deletion examples/lima/coder.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ provision:
[ ! -e ~/.config/coderv2/session ] && coder login http://localhost:3000 --first-user-username admin --first-user-email [email protected] --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"
Expand Down
3 changes: 1 addition & 2 deletions scripts/develop.sh
Original file line number Diff line number Diff line change
Expand Up @@ -151,15 +151,14 @@ 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
# template
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"
Expand Down