|
9 | 9 |
|
10 | 10 | "github.com/coder/coder/v2/cli/clibase"
|
11 | 11 | "github.com/coder/coder/v2/cli/cliui"
|
| 12 | + "github.com/coder/coder/v2/cli/config" |
12 | 13 | "github.com/coder/coder/v2/codersdk"
|
| 14 | + "github.com/coder/pretty" |
13 | 15 | )
|
14 | 16 |
|
15 | 17 | func (r *RootCmd) organizations() *clibase.Cmd {
|
@@ -52,56 +54,155 @@ func (r *RootCmd) switchOrganization() *clibase.Cmd {
|
52 | 54 | ),
|
53 | 55 | Middleware: clibase.Chain(
|
54 | 56 | r.InitClient(client),
|
| 57 | + clibase.RequireRangeArgs(0, 1), |
55 | 58 | ),
|
56 | 59 | Options: clibase.OptionSet{},
|
57 | 60 | Handler: func(inv *clibase.Invocation) error {
|
58 |
| - orgArg := inv.Args[0] |
59 | 61 | conf := r.createConfig()
|
60 | 62 | orgs, err := client.OrganizationsByUser(inv.Context(), codersdk.Me)
|
61 | 63 | if err != nil {
|
62 | 64 | return fmt.Errorf("failed to get organizations: %w", err)
|
63 | 65 | }
|
| 66 | + // Keep the list of orgs sorted |
| 67 | + slices.SortFunc(orgs, func(a, b codersdk.Organization) int { |
| 68 | + return strings.Compare(a.Name, b.Name) |
| 69 | + }) |
| 70 | + |
| 71 | + var orgArg string |
| 72 | + if len(inv.Args) == 0 { |
| 73 | + // Pull orgArg from a prompt selector, rather than command line |
| 74 | + // args. |
| 75 | + orgArg, err = promptUserSelectOrg(inv, conf, orgs) |
| 76 | + if err != nil { |
| 77 | + return err |
| 78 | + } |
| 79 | + } else { |
| 80 | + orgArg = inv.Args[0] |
| 81 | + } |
64 | 82 |
|
65 | 83 | // If the user passes an empty string, we want to remove the organization
|
| 84 | + // from the config file. This will defer to default behavior. |
66 | 85 | if orgArg == "" {
|
67 | 86 | err := conf.Organization().Delete()
|
68 | 87 | if err != nil && !errors.Is(err, os.ErrNotExist) {
|
69 | 88 | return fmt.Errorf("failed to unset organization: %w", err)
|
70 | 89 | }
|
| 90 | + _, _ = fmt.Fprintf(inv.Stdout, "Organization unset\n") |
71 | 91 | } else {
|
| 92 | + // Find the selected org in our list. |
72 | 93 | index := slices.IndexFunc(orgs, func(org codersdk.Organization) bool {
|
73 | 94 | return org.Name == orgArg || org.ID.String() == orgArg
|
74 | 95 | })
|
75 | 96 | if index < 0 {
|
76 |
| - names := make([]string, 0, len(orgs)) |
77 |
| - for _, org := range orgs { |
78 |
| - names = append(names, org.Name) |
79 |
| - } |
80 |
| - |
81 | 97 | // Using this error for better error message formatting
|
82 | 98 | err := &codersdk.Error{
|
83 | 99 | Response: codersdk.Response{
|
84 |
| - Message: fmt.Sprintf("Organization %q not found.", orgArg), |
| 100 | + Message: fmt.Sprintf("Organization %q not found. Is the name correct, and are you a member of it?", orgArg), |
85 | 101 | Detail: "Ensure the organization argument is correct and you are a member of it.",
|
86 | 102 | },
|
87 |
| - Helper: fmt.Sprintf("Valid organizations you can switch to: %q", strings.Join(names, ", ")), |
| 103 | + Helper: fmt.Sprintf("Valid organizations you can switch to: %q", strings.Join(orgNames(orgs), ", ")), |
88 | 104 | }
|
89 | 105 | return err
|
90 | 106 | }
|
91 | 107 |
|
| 108 | + // Always write the uuid to the config file. Names can change. |
92 | 109 | err := conf.Organization().Write(orgs[index].ID.String())
|
93 | 110 | if err != nil {
|
94 | 111 | return fmt.Errorf("failed to write organization to config file: %w", err)
|
95 | 112 | }
|
96 | 113 | }
|
97 | 114 |
|
| 115 | + // Verify it worked. |
| 116 | + current, err := CurrentOrganization(r, inv, client) |
| 117 | + if err != nil { |
| 118 | + // An SDK error could be a permission error. So offer the advice to unset the org |
| 119 | + // and reset the context. |
| 120 | + var sdkError *codersdk.Error |
| 121 | + if errors.As(err, &sdkError) { |
| 122 | + if sdkError.Helper == "" && sdkError.StatusCode() != 500 { |
| 123 | + sdkError.Helper = fmt.Sprintf("If this error persists, try unsetting your org with 'coder organizations switch \"\"'") |
| 124 | + } |
| 125 | + return sdkError |
| 126 | + } |
| 127 | + return fmt.Errorf("failed to get current organization: %w", err) |
| 128 | + } |
| 129 | + |
| 130 | + _, _ = fmt.Fprintf(inv.Stdout, "Current organization context set to %s (%s)\n", current.Name, current.ID.String()) |
98 | 131 | return nil
|
99 | 132 | },
|
100 | 133 | }
|
101 | 134 |
|
102 | 135 | return cmd
|
103 | 136 | }
|
104 | 137 |
|
| 138 | +// promptUserSelectOrg will prompt the user to select an organization from a list |
| 139 | +// of their organizations. |
| 140 | +func promptUserSelectOrg(inv *clibase.Invocation, conf config.Root, orgs []codersdk.Organization) (string, error) { |
| 141 | + // Default choice |
| 142 | + var defaultOrg string |
| 143 | + // Comes from config file |
| 144 | + if conf.Organization().Exists() { |
| 145 | + defaultOrg, _ = conf.Organization().Read() |
| 146 | + } |
| 147 | + |
| 148 | + // No config? Comes from default org in the list |
| 149 | + if defaultOrg == "" { |
| 150 | + defIndex := slices.IndexFunc(orgs, func(org codersdk.Organization) bool { |
| 151 | + return org.IsDefault |
| 152 | + }) |
| 153 | + if defIndex >= 0 { |
| 154 | + defaultOrg = orgs[defIndex].Name |
| 155 | + } |
| 156 | + } |
| 157 | + |
| 158 | + // Defer to first org |
| 159 | + if defaultOrg == "" && len(orgs) > 0 { |
| 160 | + defaultOrg = orgs[0].Name |
| 161 | + } |
| 162 | + |
| 163 | + // Ensure the `defaultOrg` value is an org name, not a uuid. |
| 164 | + // If it is a uuid, change it to the org name. |
| 165 | + index := slices.IndexFunc(orgs, func(org codersdk.Organization) bool { |
| 166 | + return org.ID.String() == defaultOrg || org.Name == defaultOrg |
| 167 | + }) |
| 168 | + if index >= 0 { |
| 169 | + defaultOrg = orgs[index].Name |
| 170 | + } |
| 171 | + |
| 172 | + const deselectOption = "--Deselect--" |
| 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 switch 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 | + |
105 | 206 | func (r *RootCmd) currentOrganization() *clibase.Cmd {
|
106 | 207 | var (
|
107 | 208 | client = new(codersdk.Client)
|
|
0 commit comments