Skip to content

Commit 38bec6d

Browse files
committed
Merge remote-tracking branch 'origin/main' into stevenmasley/dbauthz_on
2 parents 84ba18d + db40c29 commit 38bec6d

File tree

70 files changed

+1441
-720
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+1441
-720
lines changed

cli/cliui/parameter.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cliui
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"strings"
67

@@ -69,7 +70,28 @@ func RichParameter(cmd *cobra.Command, templateVersionParameter codersdk.Templat
6970

7071
var err error
7172
var value string
72-
if len(templateVersionParameter.Options) > 0 {
73+
if templateVersionParameter.Type == "list(string)" {
74+
// Move the cursor up a single line for nicer display!
75+
_, _ = fmt.Fprint(cmd.OutOrStdout(), "\033[1A")
76+
77+
var options []string
78+
err = json.Unmarshal([]byte(templateVersionParameter.DefaultValue), &options)
79+
if err != nil {
80+
return "", err
81+
}
82+
83+
values, err := MultiSelect(cmd, options)
84+
if err == nil {
85+
v, err := json.Marshal(&values)
86+
if err != nil {
87+
return "", err
88+
}
89+
90+
_, _ = fmt.Fprintln(cmd.OutOrStdout())
91+
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+Styles.Prompt.String()+Styles.Field.Render(strings.Join(values, ", ")))
92+
value = string(v)
93+
}
94+
} else if len(templateVersionParameter.Options) > 0 {
7395
// Move the cursor up a single line for nicer display!
7496
_, _ = fmt.Fprint(cmd.OutOrStdout(), "\033[1A")
7597
var richParameterOption *codersdk.TemplateVersionParameterOption

cli/cliui/select.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@ func init() {
3535
{{- template "option" $.IterateOption $ix $option}}
3636
{{- end}}
3737
{{- end }}`
38+
39+
survey.MultiSelectQuestionTemplate = `
40+
{{- define "option"}}
41+
{{- if eq .SelectedIndex .CurrentIndex }}{{color .Config.Icons.SelectFocus.Format }}{{ .Config.Icons.SelectFocus.Text }}{{color "reset"}}{{else}} {{end}}
42+
{{- if index .Checked .CurrentOpt.Index }}{{color .Config.Icons.MarkedOption.Format }} {{ .Config.Icons.MarkedOption.Text }} {{else}}{{color .Config.Icons.UnmarkedOption.Format }} {{ .Config.Icons.UnmarkedOption.Text }} {{end}}
43+
{{- color "reset"}}
44+
{{- " "}}{{- .CurrentOpt.Value}}
45+
{{end}}
46+
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
47+
{{- if not .ShowAnswer }}
48+
{{- "\n"}}
49+
{{- range $ix, $option := .PageEntries}}
50+
{{- template "option" $.IterateOption $ix $option}}
51+
{{- end}}
52+
{{- end}}`
3853
}
3954

4055
type SelectOptions struct {
@@ -118,6 +133,29 @@ func Select(cmd *cobra.Command, opts SelectOptions) (string, error) {
118133
return value, err
119134
}
120135

136+
func MultiSelect(cmd *cobra.Command, items []string) ([]string, error) {
137+
// Similar hack is applied to Select()
138+
if flag.Lookup("test.v") != nil {
139+
return items, nil
140+
}
141+
142+
prompt := &survey.MultiSelect{
143+
Options: items,
144+
Default: items,
145+
}
146+
147+
var values []string
148+
err := survey.AskOne(prompt, &values, survey.WithStdio(fileReadWriter{
149+
Reader: cmd.InOrStdin(),
150+
}, fileReadWriter{
151+
Writer: cmd.OutOrStdout(),
152+
}, cmd.OutOrStdout()))
153+
if errors.Is(err, terminal.InterruptErr) {
154+
return nil, Canceled
155+
}
156+
return values, err
157+
}
158+
121159
type fileReadWriter struct {
122160
io.Reader
123161
io.Writer

cli/cliui/select_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,36 @@ func newRichSelect(ptty *ptytest.PTY, opts cliui.RichSelectOptions) (string, err
8686
cmd.SetIn(ptty.Input())
8787
return value, cmd.ExecuteContext(context.Background())
8888
}
89+
90+
func TestMultiSelect(t *testing.T) {
91+
t.Parallel()
92+
t.Run("MultiSelect", func(t *testing.T) {
93+
items := []string{"aaa", "bbb", "ccc"}
94+
95+
t.Parallel()
96+
ptty := ptytest.New(t)
97+
msgChan := make(chan []string)
98+
go func() {
99+
resp, err := newMultiSelect(ptty, items)
100+
assert.NoError(t, err)
101+
msgChan <- resp
102+
}()
103+
require.Equal(t, items, <-msgChan)
104+
})
105+
}
106+
107+
func newMultiSelect(ptty *ptytest.PTY, items []string) ([]string, error) {
108+
var values []string
109+
cmd := &cobra.Command{
110+
RunE: func(cmd *cobra.Command, args []string) error {
111+
selectedItems, err := cliui.MultiSelect(cmd, items)
112+
if err == nil {
113+
values = selectedItems
114+
}
115+
return err
116+
},
117+
}
118+
cmd.SetOutput(ptty.Output())
119+
cmd.SetIn(ptty.Input())
120+
return values, cmd.ExecuteContext(context.Background())
121+
}

cli/configssh.go

Lines changed: 115 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"io"
1010
"io/fs"
11+
"net/http"
1112
"os"
1213
"path/filepath"
1314
"runtime"
@@ -48,6 +49,43 @@ type sshConfigOptions struct {
4849
sshOptions []string
4950
}
5051

52+
// addOptions expects options in the form of "option=value" or "option value".
53+
// It will override any existing option with the same key to prevent duplicates.
54+
// Invalid options will return an error.
55+
func (o *sshConfigOptions) addOptions(options ...string) error {
56+
for _, option := range options {
57+
err := o.addOption(option)
58+
if err != nil {
59+
return err
60+
}
61+
}
62+
return nil
63+
}
64+
65+
func (o *sshConfigOptions) addOption(option string) error {
66+
key, _, err := codersdk.ParseSSHConfigOption(option)
67+
if err != nil {
68+
return err
69+
}
70+
for i, existing := range o.sshOptions {
71+
// Override existing option if they share the same key.
72+
// This is case-insensitive. Parsing each time might be a little slow,
73+
// but it is ok.
74+
existingKey, _, err := codersdk.ParseSSHConfigOption(existing)
75+
if err != nil {
76+
// Don't mess with original values if there is an error.
77+
// This could have come from the user's manual edits.
78+
continue
79+
}
80+
if strings.EqualFold(existingKey, key) {
81+
o.sshOptions[i] = option
82+
return nil
83+
}
84+
}
85+
o.sshOptions = append(o.sshOptions, option)
86+
return nil
87+
}
88+
5189
func (o sshConfigOptions) equal(other sshConfigOptions) bool {
5290
// Compare without side-effects or regard to order.
5391
opt1 := slices.Clone(o.sshOptions)
@@ -139,6 +177,7 @@ func configSSH() *cobra.Command {
139177
usePreviousOpts bool
140178
dryRun bool
141179
skipProxyCommand bool
180+
userHostPrefix string
142181
)
143182
cmd := &cobra.Command{
144183
Annotations: workspaceCommand,
@@ -156,12 +195,13 @@ func configSSH() *cobra.Command {
156195
),
157196
Args: cobra.ExactArgs(0),
158197
RunE: func(cmd *cobra.Command, _ []string) error {
198+
ctx := cmd.Context()
159199
client, err := CreateClient(cmd)
160200
if err != nil {
161201
return err
162202
}
163203

164-
recvWorkspaceConfigs := sshPrepareWorkspaceConfigs(cmd.Context(), client)
204+
recvWorkspaceConfigs := sshPrepareWorkspaceConfigs(ctx, client)
165205

166206
out := cmd.OutOrStdout()
167207
if dryRun {
@@ -220,6 +260,13 @@ func configSSH() *cobra.Command {
220260
if usePreviousOpts && lastConfig != nil {
221261
sshConfigOpts = *lastConfig
222262
} else if lastConfig != nil && !sshConfigOpts.equal(*lastConfig) {
263+
for _, v := range sshConfigOpts.sshOptions {
264+
// If the user passes an invalid option, we should catch
265+
// this early.
266+
if _, _, err := codersdk.ParseSSHConfigOption(v); err != nil {
267+
return xerrors.Errorf("invalid option from flag: %w", err)
268+
}
269+
}
223270
newOpts := sshConfigOpts.asList()
224271
newOptsMsg := "\n\n New options: none"
225272
if len(newOpts) > 0 {
@@ -269,42 +316,85 @@ func configSSH() *cobra.Command {
269316
if err != nil {
270317
return xerrors.Errorf("fetch workspace configs failed: %w", err)
271318
}
319+
320+
coderdConfig, err := client.SSHConfiguration(ctx)
321+
if err != nil {
322+
// If the error is 404, this deployment does not support
323+
// this endpoint yet. Do not error, just assume defaults.
324+
// TODO: Remove this in 2 months (May 31, 2023). Just return the error
325+
// and remove this 404 check.
326+
var sdkErr *codersdk.Error
327+
if !(xerrors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusNotFound) {
328+
return xerrors.Errorf("fetch coderd config failed: %w", err)
329+
}
330+
coderdConfig.HostnamePrefix = "coder."
331+
}
332+
333+
if userHostPrefix != "" {
334+
// Override with user flag.
335+
coderdConfig.HostnamePrefix = userHostPrefix
336+
}
337+
272338
// Ensure stable sorting of output.
273339
slices.SortFunc(workspaceConfigs, func(a, b sshWorkspaceConfig) bool {
274340
return a.Name < b.Name
275341
})
276342
for _, wc := range workspaceConfigs {
277343
sort.Strings(wc.Hosts)
278344
// Write agent configuration.
279-
for _, hostname := range wc.Hosts {
280-
configOptions := []string{
281-
"Host coder." + hostname,
282-
}
283-
for _, option := range sshConfigOpts.sshOptions {
284-
configOptions = append(configOptions, "\t"+option)
285-
}
286-
configOptions = append(configOptions,
287-
"\tHostName coder."+hostname,
288-
"\tConnectTimeout=0",
289-
"\tStrictHostKeyChecking=no",
345+
for _, workspaceHostname := range wc.Hosts {
346+
sshHostname := fmt.Sprintf("%s%s", coderdConfig.HostnamePrefix, workspaceHostname)
347+
defaultOptions := []string{
348+
"HostName " + sshHostname,
349+
"ConnectTimeout=0",
350+
"StrictHostKeyChecking=no",
290351
// Without this, the "REMOTE HOST IDENTITY CHANGED"
291352
// message will appear.
292-
"\tUserKnownHostsFile=/dev/null",
353+
"UserKnownHostsFile=/dev/null",
293354
// This disables the "Warning: Permanently added 'hostname' (RSA) to the list of known hosts."
294355
// message from appearing on every SSH. This happens because we ignore the known hosts.
295-
"\tLogLevel ERROR",
296-
)
356+
"LogLevel ERROR",
357+
}
358+
297359
if !skipProxyCommand {
298-
configOptions = append(
299-
configOptions,
300-
fmt.Sprintf(
301-
"\tProxyCommand %s --global-config %s ssh --stdio %s",
302-
escapedCoderBinary, escapedGlobalConfig, hostname,
303-
),
304-
)
360+
defaultOptions = append(defaultOptions, fmt.Sprintf(
361+
"ProxyCommand %s --global-config %s ssh --stdio %s",
362+
escapedCoderBinary, escapedGlobalConfig, workspaceHostname,
363+
))
364+
}
365+
366+
var configOptions sshConfigOptions
367+
// Add standard options.
368+
err := configOptions.addOptions(defaultOptions...)
369+
if err != nil {
370+
return err
371+
}
372+
373+
// Override with deployment options
374+
for k, v := range coderdConfig.SSHConfigOptions {
375+
opt := fmt.Sprintf("%s %s", k, v)
376+
err := configOptions.addOptions(opt)
377+
if err != nil {
378+
return xerrors.Errorf("add coderd config option %q: %w", opt, err)
379+
}
380+
}
381+
// Override with flag options
382+
for _, opt := range sshConfigOpts.sshOptions {
383+
err := configOptions.addOptions(opt)
384+
if err != nil {
385+
return xerrors.Errorf("add flag config option %q: %w", opt, err)
386+
}
387+
}
388+
389+
hostBlock := []string{
390+
"Host " + sshHostname,
391+
}
392+
// Prefix with '\t'
393+
for _, v := range configOptions.sshOptions {
394+
hostBlock = append(hostBlock, "\t"+v)
305395
}
306396

307-
_, _ = buf.WriteString(strings.Join(configOptions, "\n"))
397+
_, _ = buf.WriteString(strings.Join(hostBlock, "\n"))
308398
_ = buf.WriteByte('\n')
309399
}
310400
}
@@ -363,7 +453,7 @@ func configSSH() *cobra.Command {
363453

364454
if len(workspaceConfigs) > 0 {
365455
_, _ = fmt.Fprintln(out, "You should now be able to ssh into your workspace.")
366-
_, _ = fmt.Fprintf(out, "For example, try running:\n\n\t$ ssh coder.%s\n", workspaceConfigs[0].Name)
456+
_, _ = fmt.Fprintf(out, "For example, try running:\n\n\t$ ssh %s%s\n", coderdConfig.HostnamePrefix, workspaceConfigs[0].Name)
367457
} else {
368458
_, _ = fmt.Fprint(out, "You don't have any workspaces yet, try creating one with:\n\n\t$ coder create <workspace>\n")
369459
}
@@ -376,6 +466,7 @@ func configSSH() *cobra.Command {
376466
cmd.Flags().BoolVarP(&skipProxyCommand, "skip-proxy-command", "", false, "Specifies whether the ProxyCommand option should be skipped. Useful for testing.")
377467
_ = cmd.Flags().MarkHidden("skip-proxy-command")
378468
cliflag.BoolVarP(cmd.Flags(), &usePreviousOpts, "use-previous-options", "", "CODER_SSH_USE_PREVIOUS_OPTIONS", false, "Specifies whether or not to keep options from previous run of config-ssh.")
469+
cmd.Flags().StringVarP(&userHostPrefix, "ssh-host-prefix", "", "", "Override the default host prefix.")
379470
cliui.AllowSkipPrompt(cmd)
380471

381472
return cmd

0 commit comments

Comments
 (0)