- Notifications
You must be signed in to change notification settings - Fork1k
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Merged
Uh oh!
There was an error while loading.Please reload this page.
Merged
Changes fromall commits
Commits
Show all changes
7 commits Select commitHold shift + click to select a range
356fb39
feat(cli): add CLI support for listing presets
ssncferreira641dc96
chore: address comments
ssncferreirab0b83fa
chore: run make gen
ssncferreira9856d7e
Merge remote-tracking branch 'origin/main' into ssncferreira/cli-pres…
ssncferreira37c1960
chore: simplify presets listing under coder templates presets
ssncferreira348148e
chore: add message describing used template and template-version
ssncferreiracdf2524
Merge remote-tracking branch 'origin/main' into ssncferreira/cli-pres…
ssncferreiraFile filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
168 changes: 168 additions & 0 deletionscli/templatepresets.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
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) templatePresets() *serpent.Command { | ||
cmd := &serpent.Command{ | ||
Use: "presets", | ||
Short: "Manage presets of the specified template", | ||
Aliases: []string{"preset"}, | ||
Long: FormatExamples( | ||
Example{ | ||
Description: "List presets for the active version of a template", | ||
Command: "coder templates presets list my-template", | ||
}, | ||
Example{ | ||
Description: "List presets for a specific version of a template", | ||
Command: "coder templates presets list my-template --template-version my-template-version", | ||
}, | ||
), | ||
Handler: func(inv *serpent.Invocation) error { | ||
return inv.Command.HelpHandler(inv) | ||
}, | ||
Children: []*serpent.Command{ | ||
r.templatePresetsList(), | ||
}, | ||
} | ||
return cmd | ||
} | ||
func (r *RootCmd) templatePresetsList() *serpent.Command { | ||
defaultColumns := []string{ | ||
"name", | ||
"parameters", | ||
"default", | ||
"desired prebuild instances", | ||
} | ||
formatter := cliui.NewOutputFormatter( | ||
cliui.TableFormat([]templatePresetRow{}, defaultColumns), | ||
cliui.JSONFormat(), | ||
) | ||
client := new(codersdk.Client) | ||
orgContext := NewOrganizationContext() | ||
var templateVersion string | ||
cmd := &serpent.Command{ | ||
Use: "list <template>", | ||
Middleware: serpent.Chain( | ||
serpent.RequireNArgs(1), | ||
r.InitClient(client), | ||
), | ||
Short: "List all presets of the specified template. Defaults to the active template version.", | ||
Options: serpent.OptionSet{ | ||
{ | ||
Name: "template-version", | ||
Description: "Specify a template version to list presets for. Defaults to the active version.", | ||
Flag: "template-version", | ||
Value: serpent.StringOf(&templateVersion), | ||
}, | ||
}, | ||
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) | ||
} | ||
// If a template version is specified via flag, fetch that version by name | ||
var version codersdk.TemplateVersion | ||
if len(templateVersion) > 0 { | ||
version, err = client.TemplateVersionByName(inv.Context(), template.ID, templateVersion) | ||
if err != nil { | ||
return xerrors.Errorf("get template version by name: %w", err) | ||
} | ||
} else { | ||
// Otherwise, use the template's active version | ||
version, err = client.TemplateVersion(inv.Context(), template.ActiveVersionID) | ||
if err != nil { | ||
return xerrors.Errorf("get active template version: %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 { | ||
cliui.Infof( | ||
inv.Stdout, | ||
"No presets found for template %q and template-version %q.\n", template.Name, version.Name, | ||
) | ||
return nil | ||
} | ||
cliui.Infof( | ||
inv.Stdout, | ||
"Showing presets for template %q and template version %q.\n", template.Name, version.Name, | ||
) | ||
rows := templatePresetsToRows(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 templatePresetRow struct { | ||
// For json format: | ||
TemplatePreset codersdk.Preset `table:"-"` | ||
// For table format: | ||
Name string `json:"-" table:"name,default_sort"` | ||
Parameters string `json:"-" table:"parameters"` | ||
Default bool `json:"-" table:"default"` | ||
DesiredPrebuildInstances string `json:"-" table:"desired prebuild instances"` | ||
} | ||
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, ",") | ||
} | ||
// templatePresetsToRows converts a list of presets to a list of rows | ||
// for outputting. | ||
func templatePresetsToRows(presets ...codersdk.Preset) []templatePresetRow { | ||
rows := make([]templatePresetRow, len(presets)) | ||
for i, preset := range presets { | ||
prebuildInstances := "-" | ||
if preset.DesiredPrebuildInstances != nil { | ||
prebuildInstances = strconv.Itoa(*preset.DesiredPrebuildInstances) | ||
} | ||
rows[i] = templatePresetRow{ | ||
Name: preset.Name, | ||
Parameters: formatPresetParameters(preset.Parameters), | ||
Default: preset.Default, | ||
DesiredPrebuildInstances: prebuildInstances, | ||
} | ||
} | ||
return rows | ||
} |
228 changes: 228 additions & 0 deletionscli/templatepresets_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
package cli_test | ||
import ( | ||
"fmt" | ||
"testing" | ||
"github.com/stretchr/testify/require" | ||
"github.com/coder/coder/v2/cli/clitest" | ||
"github.com/coder/coder/v2/coderd/coderdtest" | ||
"github.com/coder/coder/v2/codersdk" | ||
"github.com/coder/coder/v2/provisioner/echo" | ||
"github.com/coder/coder/v2/provisionersdk/proto" | ||
"github.com/coder/coder/v2/pty/ptytest" | ||
"github.com/coder/coder/v2/testutil" | ||
) | ||
func TestTemplatePresets(t *testing.T) { | ||
t.Parallel() | ||
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 | ||
inv, root := clitest.New(t, "templates", "presets", "list", template.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 a message when no presets are found for the given template and version. | ||
notFoundMessage := fmt.Sprintf("No presets found for template %q and template-version %q.", template.Name, version.Name) | ||
pty.ExpectRegexMatch(notFoundMessage) | ||
}) | ||
t.Run("ListsPresetsForDefaultTemplateVersion", 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: an active 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) | ||
require.Equal(t, version.ID, template.ActiveVersionID) | ||
// When: listing presets for that template | ||
inv, root := clitest.New(t, "templates", "presets", "list", template.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 active version's presets sorted by name | ||
message := fmt.Sprintf("Showing presets for template %q and template version %q.", template.Name, version.Name) | ||
pty.ExpectMatch(message) | ||
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("ListsPresetsForSpecifiedTemplateVersion", func(t *testing.T) { | ||
t.Parallel() | ||
ctx := testutil.Context(t, testutil.WaitMedium) | ||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) | ||
owner := coderdtest.CreateFirstUser(t, client) | ||
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) | ||
// Given: a template with an active version that has no presets, | ||
// and another 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, | ||
}, | ||
}, | ||
} | ||
// Given: first template version with presets | ||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithPresets(presets)) | ||
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) | ||
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) | ||
// Given: second template version without presets | ||
activeVersion := coderdtest.UpdateTemplateVersion(t, client, owner.OrganizationID, templateWithPresets([]*proto.Preset{}), template.ID) | ||
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, activeVersion.ID) | ||
// Given: second template version is the active version | ||
err := client.UpdateActiveTemplateVersion(ctx, template.ID, codersdk.UpdateActiveTemplateVersion{ | ||
ID: activeVersion.ID, | ||
}) | ||
require.NoError(t, err) | ||
updatedTemplate, err := client.Template(ctx, template.ID) | ||
require.NoError(t, err) | ||
require.Equal(t, activeVersion.ID, updatedTemplate.ActiveVersionID) | ||
// Given: template has two versions | ||
templateVersions, err := client.TemplateVersionsByTemplate(ctx, codersdk.TemplateVersionsByTemplateRequest{ | ||
TemplateID: updatedTemplate.ID, | ||
}) | ||
require.NoError(t, err) | ||
require.Len(t, templateVersions, 2) | ||
// When: listing presets for a specific template and its specified version | ||
inv, root := clitest.New(t, "templates", "presets", "list", updatedTemplate.Name, "--template-version", 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 specified version's presets sorted by name | ||
message := fmt.Sprintf("Showing presets for template %q and template version %q.", template.Name, version.Name) | ||
pty.ExpectMatch(message) | ||
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`) | ||
}) | ||
} | ||
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, | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
} |
Oops, something went wrong.
Uh oh!
There was an error while loading.Please reload this page.
Oops, something went wrong.
Uh oh!
There was an error while loading.Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.