Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
97 changes: 72 additions & 25 deletions cli/templateinit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 <template_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
Expand Down Expand Up @@ -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
}
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, "invalid choice: thistemplatedoesnotexist, should be one of")
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 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.
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>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]</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 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"
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