Skip to content

Commit 356fb39

Browse files
committed
feat(cli): add CLI support for listing presets
1 parent aae5fc2 commit 356fb39

18 files changed

+427
-11
lines changed

cli/templateversionpresets.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
8+
"golang.org/x/xerrors"
9+
10+
"github.com/coder/coder/v2/cli/cliui"
11+
"github.com/coder/coder/v2/codersdk"
12+
"github.com/coder/serpent"
13+
)
14+
15+
func (r *RootCmd) templateVersionPresets() *serpent.Command {
16+
cmd := &serpent.Command{
17+
Use: "presets",
18+
Short: "Manage presets of the specified template version",
19+
Aliases: []string{"preset"},
20+
Long: FormatExamples(
21+
Example{
22+
Description: "List presets of a specific template version",
23+
Command: "coder templates versions presets list my-template my-template-version",
24+
},
25+
),
26+
Handler: func(inv *serpent.Invocation) error {
27+
return inv.Command.HelpHandler(inv)
28+
},
29+
Children: []*serpent.Command{
30+
r.templateVersionPresetsList(),
31+
},
32+
}
33+
34+
return cmd
35+
}
36+
37+
func (r *RootCmd) templateVersionPresetsList() *serpent.Command {
38+
defaultColumns := []string{
39+
"name",
40+
"parameters",
41+
"default",
42+
"prebuilds",
43+
}
44+
formatter := cliui.NewOutputFormatter(
45+
cliui.TableFormat([]templateVersionPresetRow{}, defaultColumns),
46+
cliui.JSONFormat(),
47+
)
48+
client := new(codersdk.Client)
49+
orgContext := NewOrganizationContext()
50+
51+
cmd := &serpent.Command{
52+
Use: "list <template> <version>",
53+
Middleware: serpent.Chain(
54+
serpent.RequireNArgs(2),
55+
r.InitClient(client),
56+
),
57+
Short: "List all the presets of the specified template version",
58+
Options: serpent.OptionSet{},
59+
Handler: func(inv *serpent.Invocation) error {
60+
organization, err := orgContext.Selected(inv, client)
61+
if err != nil {
62+
return xerrors.Errorf("get current organization: %w", err)
63+
}
64+
65+
template, err := client.TemplateByName(inv.Context(), organization.ID, inv.Args[0])
66+
if err != nil {
67+
return xerrors.Errorf("get template by name: %w", err)
68+
}
69+
70+
version, err := client.TemplateVersionByName(inv.Context(), template.ID, inv.Args[1])
71+
if err != nil {
72+
return xerrors.Errorf("get template version by name: %w", err)
73+
}
74+
75+
presets, err := client.TemplateVersionPresets(inv.Context(), version.ID)
76+
if err != nil {
77+
return xerrors.Errorf("get template versions presets by template version: %w", err)
78+
}
79+
80+
if len(presets) == 0 {
81+
return xerrors.Errorf("no presets found for template %q and template-version %q", template.Name, version.Name)
82+
}
83+
84+
rows := templateVersionPresetsToRows(presets...)
85+
out, err := formatter.Format(inv.Context(), rows)
86+
if err != nil {
87+
return xerrors.Errorf("render table: %w", err)
88+
}
89+
90+
_, err = fmt.Fprintln(inv.Stdout, out)
91+
return err
92+
},
93+
}
94+
95+
orgContext.AttachOptions(cmd)
96+
formatter.AttachOptions(&cmd.Options)
97+
return cmd
98+
}
99+
100+
type templateVersionPresetRow struct {
101+
// For json format:
102+
TemplateVersionPreset codersdk.Preset `table:"-"`
103+
104+
// For table format:
105+
Name string `json:"-" table:"name,default_sort"`
106+
Parameters string `json:"-" table:"parameters"`
107+
Default bool `json:"-" table:"default"`
108+
Prebuilds string `json:"-" table:"prebuilds"`
109+
}
110+
111+
func formatPresetParameters(params []codersdk.PresetParameter) string {
112+
var paramsStr []string
113+
for _, p := range params {
114+
paramsStr = append(paramsStr, fmt.Sprintf("%s=%s", p.Name, p.Value))
115+
}
116+
return strings.Join(paramsStr, ",")
117+
}
118+
119+
// templateVersionPresetsToRows converts a list of presets to a list of rows
120+
// for outputting.
121+
func templateVersionPresetsToRows(presets ...codersdk.Preset) []templateVersionPresetRow {
122+
rows := make([]templateVersionPresetRow, len(presets))
123+
for i, preset := range presets {
124+
prebuilds := "-"
125+
if preset.Prebuilds != nil {
126+
prebuilds = strconv.Itoa(*preset.Prebuilds)
127+
}
128+
rows[i] = templateVersionPresetRow{
129+
Name: preset.Name,
130+
Parameters: formatPresetParameters(preset.Parameters),
131+
Default: preset.Default,
132+
Prebuilds: prebuilds,
133+
}
134+
}
135+
136+
return rows
137+
}

cli/templateversionpresets_test.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package cli_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/coder/coder/v2/provisioner/echo"
8+
"github.com/coder/coder/v2/provisionersdk/proto"
9+
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/coder/coder/v2/cli/clitest"
13+
"github.com/coder/coder/v2/coderd/coderdtest"
14+
"github.com/coder/coder/v2/pty/ptytest"
15+
)
16+
17+
func TestTemplateVersionPresets(t *testing.T) {
18+
t.Parallel()
19+
20+
t.Run("ListPresets", func(t *testing.T) {
21+
t.Parallel()
22+
23+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
24+
owner := coderdtest.CreateFirstUser(t, client)
25+
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
26+
27+
// Given: a template version that includes presets
28+
presets := []*proto.Preset{
29+
{
30+
Name: "preset-multiple-params",
31+
Parameters: []*proto.PresetParameter{
32+
{
33+
Name: "k1",
34+
Value: "v1",
35+
}, {
36+
Name: "k2",
37+
Value: "v2",
38+
},
39+
},
40+
},
41+
{
42+
Name: "preset-default",
43+
Default: true,
44+
Parameters: []*proto.PresetParameter{
45+
{
46+
Name: "k1",
47+
Value: "v2",
48+
},
49+
},
50+
Prebuild: &proto.Prebuild{
51+
Instances: 0,
52+
},
53+
},
54+
{
55+
Name: "preset-prebuilds",
56+
Parameters: []*proto.PresetParameter{},
57+
Prebuild: &proto.Prebuild{
58+
Instances: 2,
59+
},
60+
},
61+
}
62+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithPresets(presets))
63+
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
64+
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
65+
66+
// When: listing presets for that template and template version
67+
inv, root := clitest.New(t, "templates", "versions", "presets", "list", template.Name, version.Name)
68+
clitest.SetupConfig(t, member, root)
69+
70+
pty := ptytest.New(t).Attach(inv)
71+
doneChan := make(chan struct{})
72+
var runErr error
73+
go func() {
74+
defer close(doneChan)
75+
runErr = inv.Run()
76+
}()
77+
78+
<-doneChan
79+
require.NoError(t, runErr)
80+
81+
// Should: return the presets sorted by name
82+
pty.ExpectRegexMatch(`preset-default\s+k1=v2\s+true\s+0`)
83+
// The parameter order is not guaranteed in the output, so we match both possible orders
84+
pty.ExpectRegexMatch(`preset-multiple-params\s+(k1=v1,k2=v2)|(k2=v2,k1=v1)\s+false\s+-`)
85+
pty.ExpectRegexMatch(`preset-prebuilds\s+\s+false\s+2`)
86+
})
87+
88+
t.Run("NoPresets", func(t *testing.T) {
89+
t.Parallel()
90+
91+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
92+
owner := coderdtest.CreateFirstUser(t, client)
93+
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
94+
95+
// Given: a template version without presets
96+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithPresets([]*proto.Preset{}))
97+
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
98+
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
99+
100+
// When: listing presets for that template and template version
101+
inv, root := clitest.New(t, "templates", "versions", "presets", "list", template.Name, version.Name)
102+
clitest.SetupConfig(t, member, root)
103+
104+
ptytest.New(t).Attach(inv)
105+
doneChan := make(chan struct{})
106+
var runErr error
107+
go func() {
108+
defer close(doneChan)
109+
runErr = inv.Run()
110+
}()
111+
<-doneChan
112+
113+
// Should return an error when no presets are found for the given template and version.
114+
require.Error(t, runErr)
115+
expectedErr := fmt.Sprintf(
116+
"no presets found for template %q and template-version %q",
117+
template.Name,
118+
version.Name,
119+
)
120+
require.Contains(t, runErr.Error(), expectedErr)
121+
})
122+
}
123+
124+
func templateWithPresets(presets []*proto.Preset) *echo.Responses {
125+
return &echo.Responses{
126+
Parse: echo.ParseComplete,
127+
ProvisionPlan: []*proto.Response{
128+
{
129+
Type: &proto.Response_Plan{
130+
Plan: &proto.PlanComplete{
131+
Presets: presets,
132+
},
133+
},
134+
},
135+
},
136+
}
137+
}

cli/templateversions.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func (r *RootCmd) templateVersions() *serpent.Command {
3333
r.archiveTemplateVersion(),
3434
r.unarchiveTemplateVersion(),
3535
r.templateVersionsPromote(),
36+
r.templateVersionPresets(),
3637
},
3738
}
3839

cli/testdata/coder_templates_versions_--help.golden

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ USAGE:
1414
SUBCOMMANDS:
1515
archive Archive a template version(s).
1616
list List all the versions of the specified template
17+
presets Manage presets of the specified template version
1718
promote Promote a template version to active.
1819
unarchive Unarchive a template version(s).
1920

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
coder v0.0.0-devel
2+
3+
USAGE:
4+
coder templates versions presets
5+
6+
Manage presets of the specified template version
7+
8+
Aliases: preset
9+
10+
- List presets of a specific template version:
11+
12+
$ coder templates versions presets list my-template my-template-version
13+
14+
SUBCOMMANDS:
15+
list List all the presets of the specified template version
16+
17+
———
18+
Run `coder --help` for a list of global options.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
coder v0.0.0-devel
2+
3+
USAGE:
4+
coder templates versions presets
5+
6+
Manage presets of the specified template version
7+
8+
Aliases: preset
9+
10+
- List presets of a specific template version:
11+
12+
$ coder templates versions presets list my-template my-template-version
13+
14+
SUBCOMMANDS:
15+
list List all the presets of the specified template version
16+
17+
———
18+
Run `coder --help` for a list of global options.

coderd/apidoc/docs.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/presets.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package coderd
22

33
import (
4+
"database/sql"
45
"net/http"
56

67
"github.com/coder/coder/v2/coderd/httpapi"
@@ -38,12 +39,21 @@ func (api *API) templateVersionPresets(rw http.ResponseWriter, r *http.Request)
3839
return
3940
}
4041

42+
getPrebuildInstances := func(desiredInstances sql.NullInt32) *int {
43+
if desiredInstances.Valid {
44+
value := int(desiredInstances.Int32)
45+
return &value
46+
}
47+
return nil
48+
}
49+
4150
var res []codersdk.Preset
4251
for _, preset := range presets {
4352
sdkPreset := codersdk.Preset{
44-
ID: preset.ID,
45-
Name: preset.Name,
46-
Default: preset.IsDefault,
53+
ID: preset.ID,
54+
Name: preset.Name,
55+
Default: preset.IsDefault,
56+
Prebuilds: getPrebuildInstances(preset.DesiredInstances),
4757
}
4858
for _, presetParam := range presetParams {
4959
if presetParam.TemplateVersionPresetID != preset.ID {

codersdk/presets.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type Preset struct {
1515
Name string
1616
Parameters []PresetParameter
1717
Default bool
18+
Prebuilds *int
1819
}
1920

2021
type PresetParameter struct {

0 commit comments

Comments
 (0)