-
Notifications
You must be signed in to change notification settings - Fork 968
feat(cli): add CLI support for listing presets #18910
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
Changes from 1 commit
356fb39
641dc96
b0b83fa
9856d7e
37c1960
348148e
cdf2524
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
package cli | ||
|
||
import ( | ||
"fmt" | ||
"strconv" | ||
"strings" | ||
|
||
"golang.org/x/xerrors" | ||
|
||
"github.com/coder/coder/v2/cli/cliui" | ||
"github.com/coder/coder/v2/codersdk" | ||
"github.com/coder/serpent" | ||
) | ||
|
||
func (r *RootCmd) templateVersionPresets() *serpent.Command { | ||
cmd := &serpent.Command{ | ||
Use: "presets", | ||
Short: "Manage presets of the specified template version", | ||
Aliases: []string{"preset"}, | ||
Long: FormatExamples( | ||
Example{ | ||
Description: "List presets of a specific template version", | ||
Command: "coder templates versions presets list my-template my-template-version", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently, the We could also introduce a Example usage:
Let me know what you think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like I think template version flag should be optional with the active version as a default. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As an aside, I now see the logic of going for I'll just keep quiet about the naming now 😆 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've updated the command to I decided to place this under just |
||
}, | ||
), | ||
Handler: func(inv *serpent.Invocation) error { | ||
return inv.Command.HelpHandler(inv) | ||
}, | ||
Children: []*serpent.Command{ | ||
r.templateVersionPresetsList(), | ||
}, | ||
} | ||
|
||
return cmd | ||
} | ||
|
||
func (r *RootCmd) templateVersionPresetsList() *serpent.Command { | ||
defaultColumns := []string{ | ||
"name", | ||
"parameters", | ||
"default", | ||
"prebuilds", | ||
} | ||
formatter := cliui.NewOutputFormatter( | ||
cliui.TableFormat([]templateVersionPresetRow{}, defaultColumns), | ||
cliui.JSONFormat(), | ||
) | ||
client := new(codersdk.Client) | ||
orgContext := NewOrganizationContext() | ||
|
||
cmd := &serpent.Command{ | ||
Use: "list <template> <version>", | ||
Middleware: serpent.Chain( | ||
serpent.RequireNArgs(2), | ||
r.InitClient(client), | ||
), | ||
Short: "List all the presets of the specified template version", | ||
Options: serpent.OptionSet{}, | ||
Handler: func(inv *serpent.Invocation) error { | ||
organization, err := orgContext.Selected(inv, client) | ||
if err != nil { | ||
return xerrors.Errorf("get current organization: %w", err) | ||
} | ||
|
||
template, err := client.TemplateByName(inv.Context(), organization.ID, inv.Args[0]) | ||
if err != nil { | ||
return xerrors.Errorf("get template by name: %w", err) | ||
} | ||
|
||
version, err := client.TemplateVersionByName(inv.Context(), template.ID, inv.Args[1]) | ||
if err != nil { | ||
return xerrors.Errorf("get template version by name: %w", err) | ||
} | ||
|
||
presets, err := client.TemplateVersionPresets(inv.Context(), version.ID) | ||
if err != nil { | ||
return xerrors.Errorf("get template versions presets by template version: %w", err) | ||
} | ||
|
||
if len(presets) == 0 { | ||
return xerrors.Errorf("no presets found for template %q and template-version %q", template.Name, version.Name) | ||
ssncferreira marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
rows := templateVersionPresetsToRows(presets...) | ||
out, err := formatter.Format(inv.Context(), rows) | ||
if err != nil { | ||
return xerrors.Errorf("render table: %w", err) | ||
} | ||
|
||
_, err = fmt.Fprintln(inv.Stdout, out) | ||
return err | ||
}, | ||
} | ||
|
||
orgContext.AttachOptions(cmd) | ||
formatter.AttachOptions(&cmd.Options) | ||
return cmd | ||
} | ||
|
||
type templateVersionPresetRow struct { | ||
// For json format: | ||
TemplateVersionPreset codersdk.Preset `table:"-"` | ||
|
||
// For table format: | ||
Name string `json:"-" table:"name,default_sort"` | ||
Parameters string `json:"-" table:"parameters"` | ||
Default bool `json:"-" table:"default"` | ||
Prebuilds string `json:"-" table:"prebuilds"` | ||
ssncferreira marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
func formatPresetParameters(params []codersdk.PresetParameter) string { | ||
var paramsStr []string | ||
for _, p := range params { | ||
paramsStr = append(paramsStr, fmt.Sprintf("%s=%s", p.Name, p.Value)) | ||
} | ||
return strings.Join(paramsStr, ",") | ||
} | ||
|
||
// templateVersionPresetsToRows converts a list of presets to a list of rows | ||
// for outputting. | ||
func templateVersionPresetsToRows(presets ...codersdk.Preset) []templateVersionPresetRow { | ||
rows := make([]templateVersionPresetRow, len(presets)) | ||
for i, preset := range presets { | ||
prebuilds := "-" | ||
if preset.Prebuilds != nil { | ||
prebuilds = strconv.Itoa(*preset.Prebuilds) | ||
} | ||
rows[i] = templateVersionPresetRow{ | ||
Name: preset.Name, | ||
Parameters: formatPresetParameters(preset.Parameters), | ||
Default: preset.Default, | ||
Prebuilds: prebuilds, | ||
} | ||
} | ||
|
||
return rows | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
package cli_test | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/coder/coder/v2/provisioner/echo" | ||
"github.com/coder/coder/v2/provisionersdk/proto" | ||
|
||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/coder/coder/v2/cli/clitest" | ||
"github.com/coder/coder/v2/coderd/coderdtest" | ||
"github.com/coder/coder/v2/pty/ptytest" | ||
) | ||
|
||
func TestTemplateVersionPresets(t *testing.T) { | ||
t.Parallel() | ||
|
||
t.Run("ListPresets", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) | ||
owner := coderdtest.CreateFirstUser(t, client) | ||
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) | ||
|
||
// Given: a template version that includes presets | ||
presets := []*proto.Preset{ | ||
{ | ||
Name: "preset-multiple-params", | ||
Parameters: []*proto.PresetParameter{ | ||
{ | ||
Name: "k1", | ||
Value: "v1", | ||
}, { | ||
Name: "k2", | ||
Value: "v2", | ||
}, | ||
}, | ||
}, | ||
{ | ||
Name: "preset-default", | ||
Default: true, | ||
Parameters: []*proto.PresetParameter{ | ||
{ | ||
Name: "k1", | ||
Value: "v2", | ||
}, | ||
}, | ||
Prebuild: &proto.Prebuild{ | ||
Instances: 0, | ||
}, | ||
}, | ||
{ | ||
Name: "preset-prebuilds", | ||
Parameters: []*proto.PresetParameter{}, | ||
Prebuild: &proto.Prebuild{ | ||
Instances: 2, | ||
}, | ||
}, | ||
} | ||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithPresets(presets)) | ||
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) | ||
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) | ||
|
||
// When: listing presets for that template and template version | ||
inv, root := clitest.New(t, "templates", "versions", "presets", "list", template.Name, version.Name) | ||
clitest.SetupConfig(t, member, root) | ||
|
||
pty := ptytest.New(t).Attach(inv) | ||
doneChan := make(chan struct{}) | ||
var runErr error | ||
go func() { | ||
defer close(doneChan) | ||
runErr = inv.Run() | ||
}() | ||
|
||
<-doneChan | ||
require.NoError(t, runErr) | ||
|
||
// Should: return the presets sorted by name | ||
pty.ExpectRegexMatch(`preset-default\s+k1=v2\s+true\s+0`) | ||
// The parameter order is not guaranteed in the output, so we match both possible orders | ||
pty.ExpectRegexMatch(`preset-multiple-params\s+(k1=v1,k2=v2)|(k2=v2,k1=v1)\s+false\s+-`) | ||
pty.ExpectRegexMatch(`preset-prebuilds\s+\s+false\s+2`) | ||
}) | ||
|
||
t.Run("NoPresets", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) | ||
owner := coderdtest.CreateFirstUser(t, client) | ||
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) | ||
|
||
// Given: a template version without presets | ||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithPresets([]*proto.Preset{})) | ||
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) | ||
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) | ||
|
||
// When: listing presets for that template and template version | ||
inv, root := clitest.New(t, "templates", "versions", "presets", "list", template.Name, version.Name) | ||
clitest.SetupConfig(t, member, root) | ||
|
||
ptytest.New(t).Attach(inv) | ||
doneChan := make(chan struct{}) | ||
var runErr error | ||
go func() { | ||
defer close(doneChan) | ||
runErr = inv.Run() | ||
}() | ||
<-doneChan | ||
|
||
// Should return an error when no presets are found for the given template and version. | ||
require.Error(t, runErr) | ||
expectedErr := fmt.Sprintf( | ||
"no presets found for template %q and template-version %q", | ||
template.Name, | ||
version.Name, | ||
) | ||
require.Contains(t, runErr.Error(), expectedErr) | ||
}) | ||
} | ||
|
||
func templateWithPresets(presets []*proto.Preset) *echo.Responses { | ||
return &echo.Responses{ | ||
Parse: echo.ParseComplete, | ||
ProvisionPlan: []*proto.Response{ | ||
{ | ||
Type: &proto.Response_Plan{ | ||
Plan: &proto.PlanComplete{ | ||
Presets: presets, | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
coder v0.0.0-devel | ||
|
||
USAGE: | ||
coder templates versions presets | ||
|
||
Manage presets of the specified template version | ||
|
||
Aliases: preset | ||
|
||
- List presets of a specific template version: | ||
|
||
$ coder templates versions presets list my-template my-template-version | ||
|
||
SUBCOMMANDS: | ||
list List all the presets of the specified template version | ||
|
||
——— | ||
Run `coder --help` for a list of global options. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
coder v0.0.0-devel | ||
|
||
USAGE: | ||
coder templates versions presets | ||
|
||
Manage presets of the specified template version | ||
|
||
Aliases: preset | ||
|
||
- List presets of a specific template version: | ||
|
||
$ coder templates versions presets list my-template my-template-version | ||
|
||
SUBCOMMANDS: | ||
list List all the presets of the specified template version | ||
|
||
——— | ||
Run `coder --help` for a list of global options. |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: we're not managing anything right now, and probably never will from this command.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could change it to something like "View", but since we already have a list command with a clear description, I kept the group-level description a bit more general to stay consistent with how we structure other command groups. This also leaves room to add related subcommands in the future, if needed.