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 )
0 commit comments