Skip to content

Commit 4638e1f

Browse files
committed
cli interactive mode to build a custom role
1 parent 1ebdd6f commit 4638e1f

20 files changed

+365
-133
lines changed

cli/cliui/parameter.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
4343
return "", err
4444
}
4545

46-
values, err := MultiSelect(inv, options)
46+
values, err := MultiSelect(inv, MultiSelectOptions{
47+
Options: options,
48+
Defaults: options,
49+
})
4750
if err == nil {
4851
v, err := json.Marshal(&values)
4952
if err != nil {

cli/cliui/select.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func init() {
2121
{{- .CurrentOpt.Value}}
2222
{{- color "reset"}}
2323
{{end}}
24-
24+
{{- if .Message }}{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}{{ end }}
2525
{{- if not .ShowAnswer }}
2626
{{- if .Config.Icons.Help.Text }}
2727
{{- if .FilterMessage }}{{ "Search:" }}{{ .FilterMessage }}
@@ -44,18 +44,20 @@ func init() {
4444
{{- " "}}{{- .CurrentOpt.Value}}
4545
{{end}}
4646
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
47+
{{- if .Message }}{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}{{ end }}
4748
{{- if not .ShowAnswer }}
4849
{{- "\n"}}
4950
{{- range $ix, $option := .PageEntries}}
5051
{{- template "option" $.IterateOption $ix $option}}
5152
{{- end}}
52-
{{- end}}`
53+
{{- end }}`
5354
}
5455

5556
type SelectOptions struct {
5657
Options []string
5758
// Default will be highlighted first if it's a valid option.
5859
Default string
60+
Message string
5961
Size int
6062
HideSearch bool
6163
}
@@ -122,6 +124,7 @@ func Select(inv *serpent.Invocation, opts SelectOptions) (string, error) {
122124
Options: opts.Options,
123125
Default: defaultOption,
124126
PageSize: opts.Size,
127+
Message: opts.Message,
125128
}, &value, survey.WithIcons(func(is *survey.IconSet) {
126129
is.Help.Text = "Type to search"
127130
if opts.HideSearch {
@@ -138,15 +141,22 @@ func Select(inv *serpent.Invocation, opts SelectOptions) (string, error) {
138141
return value, err
139142
}
140143

141-
func MultiSelect(inv *serpent.Invocation, items []string) ([]string, error) {
144+
type MultiSelectOptions struct {
145+
Message string
146+
Options []string
147+
Defaults []string
148+
}
149+
150+
func MultiSelect(inv *serpent.Invocation, opts MultiSelectOptions) ([]string, error) {
142151
// Similar hack is applied to Select()
143152
if flag.Lookup("test.v") != nil {
144-
return items, nil
153+
return opts.Defaults, nil
145154
}
146155

147156
prompt := &survey.MultiSelect{
148-
Options: items,
149-
Default: items,
157+
Message: opts.Message,
158+
Options: opts.Options,
159+
Default: opts.Defaults,
150160
}
151161

152162
var values []string

cli/cliui/select_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,10 @@ func newMultiSelect(ptty *ptytest.PTY, items []string) ([]string, error) {
107107
var values []string
108108
cmd := &serpent.Command{
109109
Handler: func(inv *serpent.Invocation) error {
110-
selectedItems, err := cliui.MultiSelect(inv, items)
110+
selectedItems, err := cliui.MultiSelect(inv, cliui.MultiSelectOptions{
111+
Options: items,
112+
Defaults: items,
113+
})
111114
if err == nil {
112115
values = selectedItems
113116
}

cli/configssh.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,12 +230,12 @@ func (r *RootCmd) configSSH() *serpent.Command {
230230
Annotations: workspaceCommand,
231231
Use: "config-ssh",
232232
Short: "Add an SSH Host entry for your workspaces \"ssh coder.workspace\"",
233-
Long: formatExamples(
234-
example{
233+
Long: FormatExamples(
234+
Example{
235235
Description: "You can use -o (or --ssh-option) so set SSH options to be used for all your workspaces",
236236
Command: "coder config-ssh -o ForwardAgent=yes",
237237
},
238-
example{
238+
Example{
239239
Description: "You can use --dry-run (or -n) to see the changes that would be made",
240240
Command: "coder config-ssh --dry-run",
241241
},

cli/create.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ func (r *RootCmd) create() *serpent.Command {
3535
Annotations: workspaceCommand,
3636
Use: "create [name]",
3737
Short: "Create a workspace",
38-
Long: formatExamples(
39-
example{
38+
Long: FormatExamples(
39+
Example{
4040
Description: "Create a workspace for another user (if you have permission)",
4141
Command: "coder create <username>/<workspace_name>",
4242
},

cli/dotfiles.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ func (r *RootCmd) dotfiles() *serpent.Command {
2828
Use: "dotfiles <git_repo_url>",
2929
Middleware: serpent.RequireNArgs(1),
3030
Short: "Personalize your workspace by applying a canonical dotfiles repository",
31-
Long: formatExamples(
32-
example{
31+
Long: FormatExamples(
32+
Example{
3333
Description: "Check out and install a dotfiles repository without prompts",
3434
Command: "coder dotfiles --yes git@github.com:example/dotfiles.git",
3535
},

cli/externalauth.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ func (r *RootCmd) externalAuthAccessToken() *serpent.Command {
3535
Short: "Print auth for an external provider",
3636
Long: "Print an access-token for an external auth provider. " +
3737
"The access-token will be validated and sent to stdout with exit code 0. " +
38-
"If a valid access-token cannot be obtained, the URL to authenticate will be sent to stdout with exit code 1\n" + formatExamples(
39-
example{
38+
"If a valid access-token cannot be obtained, the URL to authenticate will be sent to stdout with exit code 1\n" + FormatExamples(
39+
Example{
4040
Description: "Ensure that the user is authenticated with GitHub before cloning.",
4141
Command: `#!/usr/bin/env sh
4242
@@ -49,7 +49,7 @@ else
4949
fi
5050
`,
5151
},
52-
example{
52+
Example{
5353
Description: "Obtain an extra property of an access token for additional metadata.",
5454
Command: "coder external-auth access-token slack --extra \"authed_user.id\"",
5555
},

cli/organization.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,12 @@ func (r *RootCmd) switchOrganization() *serpent.Command {
4343
cmd := &serpent.Command{
4444
Use: "set <organization name | ID>",
4545
Short: "set the organization used by the CLI. Pass an empty string to reset to the default organization.",
46-
Long: "set the organization used by the CLI. Pass an empty string to reset to the default organization.\n" + formatExamples(
47-
example{
46+
Long: "set the organization used by the CLI. Pass an empty string to reset to the default organization.\n" + FormatExamples(
47+
Example{
4848
Description: "Remove the current organization and defer to the default.",
4949
Command: "coder organizations set ''",
5050
},
51-
example{
51+
Example{
5252
Description: "Switch to a custom organization.",
5353
Command: "coder organizations set my-org",
5454
},

cli/portforward.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,24 +35,24 @@ func (r *RootCmd) portForward() *serpent.Command {
3535
Use: "port-forward <workspace>",
3636
Short: `Forward ports from a workspace to the local machine. For reverse port forwarding, use "coder ssh -R".`,
3737
Aliases: []string{"tunnel"},
38-
Long: formatExamples(
39-
example{
38+
Long: FormatExamples(
39+
Example{
4040
Description: "Port forward a single TCP port from 1234 in the workspace to port 5678 on your local machine",
4141
Command: "coder port-forward <workspace> --tcp 5678:1234",
4242
},
43-
example{
43+
Example{
4444
Description: "Port forward a single UDP port from port 9000 to port 9000 on your local machine",
4545
Command: "coder port-forward <workspace> --udp 9000",
4646
},
47-
example{
47+
Example{
4848
Description: "Port forward multiple TCP ports and a UDP port",
4949
Command: "coder port-forward <workspace> --tcp 8080:8080 --tcp 9000:3000 --udp 5353:53",
5050
},
51-
example{
51+
Example{
5252
Description: "Port forward multiple ports (TCP or UDP) in condensed syntax",
5353
Command: "coder port-forward <workspace> --tcp 8080,9000:3000,9090-9092,10000-10002:10010-10012",
5454
},
55-
example{
55+
Example{
5656
Description: "Port forward specifying the local address to bind to",
5757
Command: "coder port-forward <workspace> --tcp 1.2.3.4:8080:8080",
5858
},

cli/root.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,12 @@ func (r *RootCmd) Command(subcommands []*serpent.Command) (*serpent.Command, err
181181
`
182182
cmd := &serpent.Command{
183183
Use: "coder [global-flags] <subcommand>",
184-
Long: fmt.Sprintf(fmtLong, buildinfo.Version()) + formatExamples(
185-
example{
184+
Long: fmt.Sprintf(fmtLong, buildinfo.Version()) + FormatExamples(
185+
Example{
186186
Description: "Start a Coder server",
187187
Command: "coder server",
188188
},
189-
example{
189+
Example{
190190
Description: "Get started by creating a template from an example",
191191
Command: "coder templates init",
192192
},
@@ -753,16 +753,16 @@ func isTTYWriter(inv *serpent.Invocation, writer io.Writer) bool {
753753
return isatty.IsTerminal(file.Fd())
754754
}
755755

756-
// example represents a standard example for command usage, to be used
757-
// with formatExamples.
758-
type example struct {
756+
// Example represents a standard example for command usage, to be used
757+
// with FormatExamples.
758+
type Example struct {
759759
Description string
760760
Command string
761761
}
762762

763-
// formatExamples formats the examples as width wrapped bulletpoint
763+
// FormatExamples formats the examples as width wrapped bulletpoint
764764
// descriptions with the command underneath.
765-
func formatExamples(examples ...example) string {
765+
func FormatExamples(examples ...Example) string {
766766
var sb strings.Builder
767767

768768
padStyle := cliui.DefaultStyles.Wrap.With(pretty.XPad(4, 0))

cli/root_internal_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func Test_formatExamples(t *testing.T) {
4545

4646
tests := []struct {
4747
name string
48-
examples []example
48+
examples []Example
4949
wantMatches []string
5050
}{
5151
{
@@ -55,7 +55,7 @@ func Test_formatExamples(t *testing.T) {
5555
},
5656
{
5757
name: "Output examples",
58-
examples: []example{
58+
examples: []Example{
5959
{
6060
Description: "Hello world.",
6161
Command: "echo hello",
@@ -72,7 +72,7 @@ func Test_formatExamples(t *testing.T) {
7272
},
7373
{
7474
name: "No description outputs commands",
75-
examples: []example{
75+
examples: []Example{
7676
{
7777
Command: "echo hello",
7878
},
@@ -87,7 +87,7 @@ func Test_formatExamples(t *testing.T) {
8787
t.Run(tt.name, func(t *testing.T) {
8888
t.Parallel()
8989

90-
got := formatExamples(tt.examples...)
90+
got := FormatExamples(tt.examples...)
9191
if len(tt.wantMatches) == 0 {
9292
require.Empty(t, got)
9393
} else {

cli/schedule.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,8 @@ func (r *RootCmd) scheduleStart() *serpent.Command {
140140
client := new(codersdk.Client)
141141
cmd := &serpent.Command{
142142
Use: "start <workspace-name> { <start-time> [day-of-week] [location] | manual }",
143-
Long: scheduleStartDescriptionLong + "\n" + formatExamples(
144-
example{
143+
Long: scheduleStartDescriptionLong + "\n" + FormatExamples(
144+
Example{
145145
Description: "Set the workspace to start at 9:30am (in Dublin) from Monday to Friday",
146146
Command: "coder schedule start my-workspace 9:30AM Mon-Fri Europe/Dublin",
147147
},
@@ -189,8 +189,8 @@ func (r *RootCmd) scheduleStop() *serpent.Command {
189189
client := new(codersdk.Client)
190190
return &serpent.Command{
191191
Use: "stop <workspace-name> { <duration> | manual }",
192-
Long: scheduleStopDescriptionLong + "\n" + formatExamples(
193-
example{
192+
Long: scheduleStopDescriptionLong + "\n" + FormatExamples(
193+
Example{
194194
Command: "coder schedule stop my-workspace 2h30m",
195195
},
196196
),
@@ -234,8 +234,8 @@ func (r *RootCmd) scheduleOverride() *serpent.Command {
234234
overrideCmd := &serpent.Command{
235235
Use: "override-stop <workspace-name> <duration from now>",
236236
Short: "Override the stop time of a currently running workspace instance.",
237-
Long: scheduleOverrideDescriptionLong + "\n" + formatExamples(
238-
example{
237+
Long: scheduleOverrideDescriptionLong + "\n" + FormatExamples(
238+
Example{
239239
Command: "coder schedule override-stop my-workspace 90m",
240240
},
241241
),

cli/templates.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ func (r *RootCmd) templates() *serpent.Command {
1616
cmd := &serpent.Command{
1717
Use: "templates",
1818
Short: "Manage templates",
19-
Long: "Templates are written in standard Terraform and describe the infrastructure for workspaces\n" + formatExamples(
20-
example{
19+
Long: "Templates are written in standard Terraform and describe the infrastructure for workspaces\n" + FormatExamples(
20+
Example{
2121
Description: "Make changes to your template, and plan the changes",
2222
Command: "coder templates plan my-template",
2323
},
24-
example{
24+
Example{
2525
Description: "Create or push an update to the template. Your developers can update their workspaces",
2626
Command: "coder templates push my-template",
2727
},

cli/templateversions.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ func (r *RootCmd) templateVersions() *serpent.Command {
1919
Use: "versions",
2020
Short: "Manage different versions of the specified template",
2121
Aliases: []string{"version"},
22-
Long: formatExamples(
23-
example{
22+
Long: FormatExamples(
23+
Example{
2424
Description: "List versions of a specific template",
2525
Command: "coder templates versions list my-template",
2626
},

cli/tokens.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,16 @@ func (r *RootCmd) tokens() *serpent.Command {
1717
cmd := &serpent.Command{
1818
Use: "tokens",
1919
Short: "Manage personal access tokens",
20-
Long: "Tokens are used to authenticate automated clients to Coder.\n" + formatExamples(
21-
example{
20+
Long: "Tokens are used to authenticate automated clients to Coder.\n" + FormatExamples(
21+
Example{
2222
Description: "Create a token for automation",
2323
Command: "coder tokens create",
2424
},
25-
example{
25+
Example{
2626
Description: "List your tokens",
2727
Command: "coder tokens ls",
2828
},
29-
example{
29+
Example{
3030
Description: "Remove a token by ID",
3131
Command: "coder tokens rm WuoWs4ZsMX",
3232
},

cli/userlist.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ func (r *RootCmd) userSingle() *serpent.Command {
5757
cmd := &serpent.Command{
5858
Use: "show <username|user_id|'me'>",
5959
Short: "Show a single user. Use 'me' to indicate the currently authenticated user.",
60-
Long: formatExamples(
61-
example{
60+
Long: FormatExamples(
61+
Example{
6262
Command: "coder users show me",
6363
},
6464
),

cli/userstatus.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ func (r *RootCmd) createUserStatusCommand(sdkStatus codersdk.UserStatus) *serpen
4040
Use: fmt.Sprintf("%s <username|user_id>", verb),
4141
Short: short,
4242
Aliases: aliases,
43-
Long: formatExamples(
44-
example{
43+
Long: FormatExamples(
44+
Example{
4545
Command: fmt.Sprintf("coder users %s example_user", verb),
4646
},
4747
),

coderd/util/slice/slice.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ import (
44
"golang.org/x/exp/constraints"
55
)
66

7+
// ToStrings works for any type where the base type is a string.
8+
func ToStrings[T ~string](a []T) []string {
9+
tmp := make([]string, 0, len(a))
10+
for _, v := range a {
11+
tmp = append(tmp, string(v))
12+
}
13+
return tmp
14+
}
15+
716
// Omit creates a new slice with the arguments omitted from the list.
817
func Omit[T comparable](a []T, omits ...T) []T {
918
tmp := make([]T, 0, len(a))

0 commit comments

Comments
 (0)