Skip to content

Commit 931b97c

Browse files
authored
feat(cli): add CLI support for listing presets (#18910)
## Description This PR introduces a new `list presets` command to display the presets associated with a given template. By default, it displays the presets for the template's active version, unless a `--template-version` flag is provided. ## Changes * Added a new `list presets` command under `coder templates presets` to display presets associated with a template. * By default, the command lists presets from the template’s active version. * Users can override the default behavior by providing the `--template-version` flag to target a specific version. ``` > coder templates versions presets list --help USAGE: coder templates presets list [flags] <template> List all presets of the specified template. Defaults to the active template version. OPTIONS: -O, --org string, $CODER_ORGANIZATION Select which organization (uuid or name) to use. -c, --column [name|parameters|default|desired prebuild instances] (default: name,parameters,default,desired prebuild instances) Columns to display in table output. -o, --output table|json (default: table) Output format. --template-version string Specify a template version to list presets for. Defaults to the active version. ``` Related PR: #18912 - please consider both PRs together as they’re part of the same workflow Relates to issue: #16594 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added CLI commands to manage and list presets for specific template versions, supporting tabular and JSON output. * Introduced a new CLI subcommand group for template version presets, including detailed help and documentation. * Added support for displaying and managing the desired number of prebuild instances for presets in CLI, API, and UI. * **Documentation** * Updated and expanded CLI and API documentation to describe new commands, options, and the desired prebuild instances field in presets. * Added new help output and reference files for template version presets commands. * **Bug Fixes** * Ensured correct handling and display of the desired prebuild instances property for presets across CLI, API, and UI. * **Tests** * Introduced end-to-end tests for listing template version presets, covering scenarios with and without presets. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 070178c commit 931b97c

18 files changed

+584
-22
lines changed

cli/templatepresets.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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) templatePresets() *serpent.Command {
16+
cmd := &serpent.Command{
17+
Use: "presets",
18+
Short: "Manage presets of the specified template",
19+
Aliases: []string{"preset"},
20+
Long: FormatExamples(
21+
Example{
22+
Description: "List presets for the active version of a template",
23+
Command: "coder templates presets list my-template",
24+
},
25+
Example{
26+
Description: "List presets for a specific version of a template",
27+
Command: "coder templates presets list my-template --template-version my-template-version",
28+
},
29+
),
30+
Handler: func(inv *serpent.Invocation) error {
31+
return inv.Command.HelpHandler(inv)
32+
},
33+
Children: []*serpent.Command{
34+
r.templatePresetsList(),
35+
},
36+
}
37+
38+
return cmd
39+
}
40+
41+
func (r *RootCmd) templatePresetsList() *serpent.Command {
42+
defaultColumns := []string{
43+
"name",
44+
"parameters",
45+
"default",
46+
"desired prebuild instances",
47+
}
48+
formatter := cliui.NewOutputFormatter(
49+
cliui.TableFormat([]templatePresetRow{}, defaultColumns),
50+
cliui.JSONFormat(),
51+
)
52+
client := new(codersdk.Client)
53+
orgContext := NewOrganizationContext()
54+
55+
var templateVersion string
56+
57+
cmd := &serpent.Command{
58+
Use: "list <template>",
59+
Middleware: serpent.Chain(
60+
serpent.RequireNArgs(1),
61+
r.InitClient(client),
62+
),
63+
Short: "List all presets of the specified template. Defaults to the active template version.",
64+
Options: serpent.OptionSet{
65+
{
66+
Name: "template-version",
67+
Description: "Specify a template version to list presets for. Defaults to the active version.",
68+
Flag: "template-version",
69+
Value: serpent.StringOf(&templateVersion),
70+
},
71+
},
72+
Handler: func(inv *serpent.Invocation) error {
73+
organization, err := orgContext.Selected(inv, client)
74+
if err != nil {
75+
return xerrors.Errorf("get current organization: %w", err)
76+
}
77+
78+
template, err := client.TemplateByName(inv.Context(), organization.ID, inv.Args[0])
79+
if err != nil {
80+
return xerrors.Errorf("get template by name: %w", err)
81+
}
82+
83+
// If a template version is specified via flag, fetch that version by name
84+
var version codersdk.TemplateVersion
85+
if len(templateVersion) > 0 {
86+
version, err = client.TemplateVersionByName(inv.Context(), template.ID, templateVersion)
87+
if err != nil {
88+
return xerrors.Errorf("get template version by name: %w", err)
89+
}
90+
} else {
91+
// Otherwise, use the template's active version
92+
version, err = client.TemplateVersion(inv.Context(), template.ActiveVersionID)
93+
if err != nil {
94+
return xerrors.Errorf("get active template version: %w", err)
95+
}
96+
}
97+
98+
presets, err := client.TemplateVersionPresets(inv.Context(), version.ID)
99+
if err != nil {
100+
return xerrors.Errorf("get template versions presets by template version: %w", err)
101+
}
102+
103+
if len(presets) == 0 {
104+
cliui.Infof(
105+
inv.Stdout,
106+
"No presets found for template %q and template-version %q.\n", template.Name, version.Name,
107+
)
108+
return nil
109+
}
110+
111+
cliui.Infof(
112+
inv.Stdout,
113+
"Showing presets for template %q and template version %q.\n", template.Name, version.Name,
114+
)
115+
rows := templatePresetsToRows(presets...)
116+
out, err := formatter.Format(inv.Context(), rows)
117+
if err != nil {
118+
return xerrors.Errorf("render table: %w", err)
119+
}
120+
121+
_, err = fmt.Fprintln(inv.Stdout, out)
122+
return err
123+
},
124+
}
125+
126+
orgContext.AttachOptions(cmd)
127+
formatter.AttachOptions(&cmd.Options)
128+
return cmd
129+
}
130+
131+
type templatePresetRow struct {
132+
// For json format:
133+
TemplatePreset codersdk.Preset `table:"-"`
134+
135+
// For table format:
136+
Name string `json:"-" table:"name,default_sort"`
137+
Parameters string `json:"-" table:"parameters"`
138+
Default bool `json:"-" table:"default"`
139+
DesiredPrebuildInstances string `json:"-" table:"desired prebuild instances"`
140+
}
141+
142+
func formatPresetParameters(params []codersdk.PresetParameter) string {
143+
var paramsStr []string
144+
for _, p := range params {
145+
paramsStr = append(paramsStr, fmt.Sprintf("%s=%s", p.Name, p.Value))
146+
}
147+
return strings.Join(paramsStr, ",")
148+
}
149+
150+
// templatePresetsToRows converts a list of presets to a list of rows
151+
// for outputting.
152+
func templatePresetsToRows(presets ...codersdk.Preset) []templatePresetRow {
153+
rows := make([]templatePresetRow, len(presets))
154+
for i, preset := range presets {
155+
prebuildInstances := "-"
156+
if preset.DesiredPrebuildInstances != nil {
157+
prebuildInstances = strconv.Itoa(*preset.DesiredPrebuildInstances)
158+
}
159+
rows[i] = templatePresetRow{
160+
Name: preset.Name,
161+
Parameters: formatPresetParameters(preset.Parameters),
162+
Default: preset.Default,
163+
DesiredPrebuildInstances: prebuildInstances,
164+
}
165+
}
166+
167+
return rows
168+
}

cli/templatepresets_test.go

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
package cli_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/coder/coder/v2/cli/clitest"
10+
"github.com/coder/coder/v2/coderd/coderdtest"
11+
"github.com/coder/coder/v2/codersdk"
12+
"github.com/coder/coder/v2/provisioner/echo"
13+
"github.com/coder/coder/v2/provisionersdk/proto"
14+
"github.com/coder/coder/v2/pty/ptytest"
15+
"github.com/coder/coder/v2/testutil"
16+
)
17+
18+
func TestTemplatePresets(t *testing.T) {
19+
t.Parallel()
20+
21+
t.Run("NoPresets", func(t *testing.T) {
22+
t.Parallel()
23+
24+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
25+
owner := coderdtest.CreateFirstUser(t, client)
26+
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
27+
28+
// Given: a template version without presets
29+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithPresets([]*proto.Preset{}))
30+
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
31+
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
32+
33+
// When: listing presets for that template
34+
inv, root := clitest.New(t, "templates", "presets", "list", template.Name)
35+
clitest.SetupConfig(t, member, root)
36+
37+
pty := ptytest.New(t).Attach(inv)
38+
doneChan := make(chan struct{})
39+
var runErr error
40+
go func() {
41+
defer close(doneChan)
42+
runErr = inv.Run()
43+
}()
44+
<-doneChan
45+
require.NoError(t, runErr)
46+
47+
// Should return a message when no presets are found for the given template and version.
48+
notFoundMessage := fmt.Sprintf("No presets found for template %q and template-version %q.", template.Name, version.Name)
49+
pty.ExpectRegexMatch(notFoundMessage)
50+
})
51+
52+
t.Run("ListsPresetsForDefaultTemplateVersion", func(t *testing.T) {
53+
t.Parallel()
54+
55+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
56+
owner := coderdtest.CreateFirstUser(t, client)
57+
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
58+
59+
// Given: an active template version that includes presets
60+
presets := []*proto.Preset{
61+
{
62+
Name: "preset-multiple-params",
63+
Parameters: []*proto.PresetParameter{
64+
{
65+
Name: "k1",
66+
Value: "v1",
67+
}, {
68+
Name: "k2",
69+
Value: "v2",
70+
},
71+
},
72+
},
73+
{
74+
Name: "preset-default",
75+
Default: true,
76+
Parameters: []*proto.PresetParameter{
77+
{
78+
Name: "k1",
79+
Value: "v2",
80+
},
81+
},
82+
Prebuild: &proto.Prebuild{
83+
Instances: 0,
84+
},
85+
},
86+
{
87+
Name: "preset-prebuilds",
88+
Parameters: []*proto.PresetParameter{},
89+
Prebuild: &proto.Prebuild{
90+
Instances: 2,
91+
},
92+
},
93+
}
94+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithPresets(presets))
95+
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
96+
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
97+
require.Equal(t, version.ID, template.ActiveVersionID)
98+
99+
// When: listing presets for that template
100+
inv, root := clitest.New(t, "templates", "presets", "list", template.Name)
101+
clitest.SetupConfig(t, member, root)
102+
103+
pty := ptytest.New(t).Attach(inv)
104+
doneChan := make(chan struct{})
105+
var runErr error
106+
go func() {
107+
defer close(doneChan)
108+
runErr = inv.Run()
109+
}()
110+
111+
<-doneChan
112+
require.NoError(t, runErr)
113+
114+
// Should: return the active version's presets sorted by name
115+
message := fmt.Sprintf("Showing presets for template %q and template version %q.", template.Name, version.Name)
116+
pty.ExpectMatch(message)
117+
pty.ExpectRegexMatch(`preset-default\s+k1=v2\s+true\s+0`)
118+
// The parameter order is not guaranteed in the output, so we match both possible orders
119+
pty.ExpectRegexMatch(`preset-multiple-params\s+(k1=v1,k2=v2)|(k2=v2,k1=v1)\s+false\s+-`)
120+
pty.ExpectRegexMatch(`preset-prebuilds\s+\s+false\s+2`)
121+
})
122+
123+
t.Run("ListsPresetsForSpecifiedTemplateVersion", func(t *testing.T) {
124+
t.Parallel()
125+
126+
ctx := testutil.Context(t, testutil.WaitMedium)
127+
128+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
129+
owner := coderdtest.CreateFirstUser(t, client)
130+
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
131+
132+
// Given: a template with an active version that has no presets,
133+
// and another template version that includes presets
134+
presets := []*proto.Preset{
135+
{
136+
Name: "preset-multiple-params",
137+
Parameters: []*proto.PresetParameter{
138+
{
139+
Name: "k1",
140+
Value: "v1",
141+
}, {
142+
Name: "k2",
143+
Value: "v2",
144+
},
145+
},
146+
},
147+
{
148+
Name: "preset-default",
149+
Default: true,
150+
Parameters: []*proto.PresetParameter{
151+
{
152+
Name: "k1",
153+
Value: "v2",
154+
},
155+
},
156+
Prebuild: &proto.Prebuild{
157+
Instances: 0,
158+
},
159+
},
160+
{
161+
Name: "preset-prebuilds",
162+
Parameters: []*proto.PresetParameter{},
163+
Prebuild: &proto.Prebuild{
164+
Instances: 2,
165+
},
166+
},
167+
}
168+
// Given: first template version with presets
169+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithPresets(presets))
170+
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
171+
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
172+
// Given: second template version without presets
173+
activeVersion := coderdtest.UpdateTemplateVersion(t, client, owner.OrganizationID, templateWithPresets([]*proto.Preset{}), template.ID)
174+
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, activeVersion.ID)
175+
// Given: second template version is the active version
176+
err := client.UpdateActiveTemplateVersion(ctx, template.ID, codersdk.UpdateActiveTemplateVersion{
177+
ID: activeVersion.ID,
178+
})
179+
require.NoError(t, err)
180+
updatedTemplate, err := client.Template(ctx, template.ID)
181+
require.NoError(t, err)
182+
require.Equal(t, activeVersion.ID, updatedTemplate.ActiveVersionID)
183+
// Given: template has two versions
184+
templateVersions, err := client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{
185+
TemplateID: updatedTemplate.ID,
186+
})
187+
require.NoError(t, err)
188+
require.Len(t, templateVersions, 2)
189+
190+
// When: listing presets for a specific template and its specified version
191+
inv, root := clitest.New(t, "templates", "presets", "list", updatedTemplate.Name, "--template-version", version.Name)
192+
clitest.SetupConfig(t, member, root)
193+
194+
pty := ptytest.New(t).Attach(inv)
195+
doneChan := make(chan struct{})
196+
var runErr error
197+
go func() {
198+
defer close(doneChan)
199+
runErr = inv.Run()
200+
}()
201+
202+
<-doneChan
203+
require.NoError(t, runErr)
204+
205+
// Should: return the specified version's presets sorted by name
206+
message := fmt.Sprintf("Showing presets for template %q and template version %q.", template.Name, version.Name)
207+
pty.ExpectMatch(message)
208+
pty.ExpectRegexMatch(`preset-default\s+k1=v2\s+true\s+0`)
209+
// The parameter order is not guaranteed in the output, so we match both possible orders
210+
pty.ExpectRegexMatch(`preset-multiple-params\s+(k1=v1,k2=v2)|(k2=v2,k1=v1)\s+false\s+-`)
211+
pty.ExpectRegexMatch(`preset-prebuilds\s+\s+false\s+2`)
212+
})
213+
}
214+
215+
func templateWithPresets(presets []*proto.Preset) *echo.Responses {
216+
return &echo.Responses{
217+
Parse: echo.ParseComplete,
218+
ProvisionPlan: []*proto.Response{
219+
{
220+
Type: &proto.Response_Plan{
221+
Plan: &proto.PlanComplete{
222+
Presets: presets,
223+
},
224+
},
225+
},
226+
},
227+
}
228+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy