1
1
package cli
2
2
3
3
import (
4
+ "errors"
4
5
"fmt"
6
+ "os"
7
+ "slices"
5
8
"strings"
6
9
7
10
"github.com/coder/coder/v2/cli/clibase"
8
11
"github.com/coder/coder/v2/cli/cliui"
12
+ "github.com/coder/coder/v2/cli/config"
9
13
"github.com/coder/coder/v2/codersdk"
14
+ "github.com/coder/pretty"
10
15
)
11
16
12
17
func (r * RootCmd )organizations ()* clibase.Cmd {
@@ -21,13 +26,183 @@ func (r *RootCmd) organizations() *clibase.Cmd {
21
26
},
22
27
Children : []* clibase.Cmd {
23
28
r .currentOrganization (),
29
+ r .switchOrganization (),
24
30
},
25
31
}
26
32
27
33
cmd .Options = clibase.OptionSet {}
28
34
return cmd
29
35
}
30
36
37
+ func (r * RootCmd )switchOrganization ()* clibase.Cmd {
38
+ client := new (codersdk.Client )
39
+
40
+ cmd := & clibase.Cmd {
41
+ Use :"set <organization name | ID>" ,
42
+ Short :"set the organization used by the CLI. Pass an empty string to reset to the default organization." ,
43
+ Long :"set the organization used by the CLI. Pass an empty string to reset to the default organization.\n " + formatExamples (
44
+ example {
45
+ Description :"Remove the current organization and defer to the default." ,
46
+ Command :"coder organizations set ''" ,
47
+ },
48
+ example {
49
+ Description :"Switch to a custom organization." ,
50
+ Command :"coder organizations set my-org" ,
51
+ },
52
+ ),
53
+ Middleware :clibase .Chain (
54
+ r .InitClient (client ),
55
+ clibase .RequireRangeArgs (0 ,1 ),
56
+ ),
57
+ Options : clibase.OptionSet {},
58
+ Handler :func (inv * clibase.Invocation )error {
59
+ conf := r .createConfig ()
60
+ orgs ,err := client .OrganizationsByUser (inv .Context (),codersdk .Me )
61
+ if err != nil {
62
+ return fmt .Errorf ("failed to get organizations: %w" ,err )
63
+ }
64
+ // Keep the list of orgs sorted
65
+ slices .SortFunc (orgs ,func (a ,b codersdk.Organization )int {
66
+ return strings .Compare (a .Name ,b .Name )
67
+ })
68
+
69
+ var switchToOrg string
70
+ if len (inv .Args )== 0 {
71
+ // Pull switchToOrg from a prompt selector, rather than command line
72
+ // args.
73
+ switchToOrg ,err = promptUserSelectOrg (inv ,conf ,orgs )
74
+ if err != nil {
75
+ return err
76
+ }
77
+ }else {
78
+ switchToOrg = inv .Args [0 ]
79
+ }
80
+
81
+ // If the user passes an empty string, we want to remove the organization
82
+ // from the config file. This will defer to default behavior.
83
+ if switchToOrg == "" {
84
+ err := conf .Organization ().Delete ()
85
+ if err != nil && ! errors .Is (err ,os .ErrNotExist ) {
86
+ return fmt .Errorf ("failed to unset organization: %w" ,err )
87
+ }
88
+ _ ,_ = fmt .Fprintf (inv .Stdout ,"Organization unset\n " )
89
+ }else {
90
+ // Find the selected org in our list.
91
+ index := slices .IndexFunc (orgs ,func (org codersdk.Organization )bool {
92
+ return org .Name == switchToOrg || org .ID .String ()== switchToOrg
93
+ })
94
+ if index < 0 {
95
+ // Using this error for better error message formatting
96
+ err := & codersdk.Error {
97
+ Response : codersdk.Response {
98
+ Message :fmt .Sprintf ("Organization %q not found. Is the name correct, and are you a member of it?" ,switchToOrg ),
99
+ Detail :"Ensure the organization argument is correct and you are a member of it." ,
100
+ },
101
+ Helper :fmt .Sprintf ("Valid organizations you can switch to: %s" ,strings .Join (orgNames (orgs ),", " )),
102
+ }
103
+ return err
104
+ }
105
+
106
+ // Always write the uuid to the config file. Names can change.
107
+ err := conf .Organization ().Write (orgs [index ].ID .String ())
108
+ if err != nil {
109
+ return fmt .Errorf ("failed to write organization to config file: %w" ,err )
110
+ }
111
+ }
112
+
113
+ // Verify it worked.
114
+ current ,err := CurrentOrganization (r ,inv ,client )
115
+ if err != nil {
116
+ // An SDK error could be a permission error. So offer the advice to unset the org
117
+ // and reset the context.
118
+ var sdkError * codersdk.Error
119
+ if errors .As (err ,& sdkError ) {
120
+ if sdkError .Helper == "" && sdkError .StatusCode ()!= 500 {
121
+ sdkError .Helper = `If this error persists, try unsetting your org with 'coder organizations set ""'`
122
+ }
123
+ return sdkError
124
+ }
125
+ return fmt .Errorf ("failed to get current organization: %w" ,err )
126
+ }
127
+
128
+ _ ,_ = fmt .Fprintf (inv .Stdout ,"Current organization context set to %s (%s)\n " ,current .Name ,current .ID .String ())
129
+ return nil
130
+ },
131
+ }
132
+
133
+ return cmd
134
+ }
135
+
136
+ // promptUserSelectOrg will prompt the user to select an organization from a list
137
+ // of their organizations.
138
+ func promptUserSelectOrg (inv * clibase.Invocation ,conf config.Root ,orgs []codersdk.Organization ) (string ,error ) {
139
+ // Default choice
140
+ var defaultOrg string
141
+ // Comes from config file
142
+ if conf .Organization ().Exists () {
143
+ defaultOrg ,_ = conf .Organization ().Read ()
144
+ }
145
+
146
+ // No config? Comes from default org in the list
147
+ if defaultOrg == "" {
148
+ defIndex := slices .IndexFunc (orgs ,func (org codersdk.Organization )bool {
149
+ return org .IsDefault
150
+ })
151
+ if defIndex >= 0 {
152
+ defaultOrg = orgs [defIndex ].Name
153
+ }
154
+ }
155
+
156
+ // Defer to first org
157
+ if defaultOrg == "" && len (orgs )> 0 {
158
+ defaultOrg = orgs [0 ].Name
159
+ }
160
+
161
+ // Ensure the `defaultOrg` value is an org name, not a uuid.
162
+ // If it is a uuid, change it to the org name.
163
+ index := slices .IndexFunc (orgs ,func (org codersdk.Organization )bool {
164
+ return org .ID .String ()== defaultOrg || org .Name == defaultOrg
165
+ })
166
+ if index >= 0 {
167
+ defaultOrg = orgs [index ].Name
168
+ }
169
+
170
+ // deselectOption is the option to delete the organization config file and defer
171
+ // to default behavior.
172
+ const deselectOption = "[Default]"
173
+ if defaultOrg == "" {
174
+ defaultOrg = deselectOption
175
+ }
176
+
177
+ // Pull value from a prompt
178
+ _ ,_ = fmt .Fprintln (inv .Stdout ,pretty .Sprint (cliui .DefaultStyles .Wrap ,"Select an organization below to set the current CLI context to:" ))
179
+ value ,err := cliui .Select (inv , cliui.SelectOptions {
180
+ Options :append ([]string {deselectOption },orgNames (orgs )... ),
181
+ Default :defaultOrg ,
182
+ Size :10 ,
183
+ HideSearch :false ,
184
+ })
185
+ if err != nil {
186
+ return "" ,err
187
+ }
188
+ // Deselect is an alias for ""
189
+ if value == deselectOption {
190
+ value = ""
191
+ }
192
+
193
+ return value ,nil
194
+ }
195
+
196
+ // orgNames is a helper function to turn a list of organizations into a list of
197
+ // their names as strings.
198
+ func orgNames (orgs []codersdk.Organization ) []string {
199
+ names := make ([]string ,0 ,len (orgs ))
200
+ for _ ,org := range orgs {
201
+ names = append (names ,org .Name )
202
+ }
203
+ return names
204
+ }
205
+
31
206
func (r * RootCmd )currentOrganization ()* clibase.Cmd {
32
207
var (
33
208
stringFormat func (orgs []codersdk.Organization ) (string ,error )