coder/cli/organization.go

310 lines
9.2 KiB
Go

package cli
import (
"errors"
"fmt"
"os"
"slices"
"strings"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/config"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
func (r *RootCmd) organizations() *serpent.Command {
cmd := &serpent.Command{
Annotations: workspaceCommand,
Use: "organizations [subcommand]",
Short: "Organization related commands",
Aliases: []string{"organization", "org", "orgs"},
Hidden: true, // Hidden until these commands are complete.
Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv)
},
Children: []*serpent.Command{
r.currentOrganization(),
r.switchOrganization(),
r.createOrganization(),
},
}
cmd.Options = serpent.OptionSet{}
return cmd
}
func (r *RootCmd) switchOrganization() *serpent.Command {
client := new(codersdk.Client)
cmd := &serpent.Command{
Use: "set <organization name | ID>",
Short: "set the organization used by the CLI. Pass an empty string to reset to the default organization.",
Long: "set the organization used by the CLI. Pass an empty string to reset to the default organization.\n" + formatExamples(
example{
Description: "Remove the current organization and defer to the default.",
Command: "coder organizations set ''",
},
example{
Description: "Switch to a custom organization.",
Command: "coder organizations set my-org",
},
),
Middleware: serpent.Chain(
r.InitClient(client),
serpent.RequireRangeArgs(0, 1),
),
Options: serpent.OptionSet{},
Handler: func(inv *serpent.Invocation) error {
conf := r.createConfig()
orgs, err := client.OrganizationsByUser(inv.Context(), codersdk.Me)
if err != nil {
return xerrors.Errorf("failed to get organizations: %w", err)
}
// Keep the list of orgs sorted
slices.SortFunc(orgs, func(a, b codersdk.Organization) int {
return strings.Compare(a.Name, b.Name)
})
var switchToOrg string
if len(inv.Args) == 0 {
// Pull switchToOrg from a prompt selector, rather than command line
// args.
switchToOrg, err = promptUserSelectOrg(inv, conf, orgs)
if err != nil {
return err
}
} else {
switchToOrg = inv.Args[0]
}
// If the user passes an empty string, we want to remove the organization
// from the config file. This will defer to default behavior.
if switchToOrg == "" {
err := conf.Organization().Delete()
if err != nil && !errors.Is(err, os.ErrNotExist) {
return xerrors.Errorf("failed to unset organization: %w", err)
}
_, _ = fmt.Fprintf(inv.Stdout, "Organization unset\n")
} else {
// Find the selected org in our list.
index := slices.IndexFunc(orgs, func(org codersdk.Organization) bool {
return org.Name == switchToOrg || org.ID.String() == switchToOrg
})
if index < 0 {
// Using this error for better error message formatting
err := &codersdk.Error{
Response: codersdk.Response{
Message: fmt.Sprintf("Organization %q not found. Is the name correct, and are you a member of it?", switchToOrg),
Detail: "Ensure the organization argument is correct and you are a member of it.",
},
Helper: fmt.Sprintf("Valid organizations you can switch to: %s", strings.Join(orgNames(orgs), ", ")),
}
return err
}
// Always write the uuid to the config file. Names can change.
err := conf.Organization().Write(orgs[index].ID.String())
if err != nil {
return xerrors.Errorf("failed to write organization to config file: %w", err)
}
}
// Verify it worked.
current, err := CurrentOrganization(r, inv, client)
if err != nil {
// An SDK error could be a permission error. So offer the advice to unset the org
// and reset the context.
var sdkError *codersdk.Error
if errors.As(err, &sdkError) {
if sdkError.Helper == "" && sdkError.StatusCode() != 500 {
sdkError.Helper = `If this error persists, try unsetting your org with 'coder organizations set ""'`
}
return sdkError
}
return xerrors.Errorf("failed to get current organization: %w", err)
}
_, _ = fmt.Fprintf(inv.Stdout, "Current organization context set to %s (%s)\n", current.Name, current.ID.String())
return nil
},
}
return cmd
}
// promptUserSelectOrg will prompt the user to select an organization from a list
// of their organizations.
func promptUserSelectOrg(inv *serpent.Invocation, conf config.Root, orgs []codersdk.Organization) (string, error) {
// Default choice
var defaultOrg string
// Comes from config file
if conf.Organization().Exists() {
defaultOrg, _ = conf.Organization().Read()
}
// No config? Comes from default org in the list
if defaultOrg == "" {
defIndex := slices.IndexFunc(orgs, func(org codersdk.Organization) bool {
return org.IsDefault
})
if defIndex >= 0 {
defaultOrg = orgs[defIndex].Name
}
}
// Defer to first org
if defaultOrg == "" && len(orgs) > 0 {
defaultOrg = orgs[0].Name
}
// Ensure the `defaultOrg` value is an org name, not a uuid.
// If it is a uuid, change it to the org name.
index := slices.IndexFunc(orgs, func(org codersdk.Organization) bool {
return org.ID.String() == defaultOrg || org.Name == defaultOrg
})
if index >= 0 {
defaultOrg = orgs[index].Name
}
// deselectOption is the option to delete the organization config file and defer
// to default behavior.
const deselectOption = "[Default]"
if defaultOrg == "" {
defaultOrg = deselectOption
}
// Pull value from a prompt
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Wrap, "Select an organization below to set the current CLI context to:"))
value, err := cliui.Select(inv, cliui.SelectOptions{
Options: append([]string{deselectOption}, orgNames(orgs)...),
Default: defaultOrg,
Size: 10,
HideSearch: false,
})
if err != nil {
return "", err
}
// Deselect is an alias for ""
if value == deselectOption {
value = ""
}
return value, nil
}
// orgNames is a helper function to turn a list of organizations into a list of
// their names as strings.
func orgNames(orgs []codersdk.Organization) []string {
names := make([]string, 0, len(orgs))
for _, org := range orgs {
names = append(names, org.Name)
}
return names
}
func (r *RootCmd) currentOrganization() *serpent.Command {
var (
stringFormat func(orgs []codersdk.Organization) (string, error)
client = new(codersdk.Client)
formatter = cliui.NewOutputFormatter(
cliui.ChangeFormatterData(cliui.TextFormat(), func(data any) (any, error) {
typed, ok := data.([]codersdk.Organization)
if !ok {
// This should never happen
return "", xerrors.Errorf("expected []Organization, got %T", data)
}
return stringFormat(typed)
}),
cliui.TableFormat([]codersdk.Organization{}, []string{"id", "name", "default"}),
cliui.JSONFormat(),
)
onlyID = false
)
cmd := &serpent.Command{
Use: "show [current|me|uuid]",
Short: "Show the organization, if no argument is given, the organization currently in use will be shown.",
Middleware: serpent.Chain(
r.InitClient(client),
serpent.RequireRangeArgs(0, 1),
),
Options: serpent.OptionSet{
{
Name: "only-id",
Description: "Only print the organization ID.",
Required: false,
Flag: "only-id",
Value: serpent.BoolOf(&onlyID),
},
},
Handler: func(inv *serpent.Invocation) error {
orgArg := "current"
if len(inv.Args) >= 1 {
orgArg = inv.Args[0]
}
var orgs []codersdk.Organization
var err error
switch strings.ToLower(orgArg) {
case "current":
stringFormat = func(orgs []codersdk.Organization) (string, error) {
if len(orgs) != 1 {
return "", xerrors.Errorf("expected 1 organization, got %d", len(orgs))
}
return fmt.Sprintf("Current CLI Organization: %s (%s)\n", orgs[0].Name, orgs[0].ID.String()), nil
}
org, err := CurrentOrganization(r, inv, client)
if err != nil {
return err
}
orgs = []codersdk.Organization{org}
case "me":
stringFormat = func(orgs []codersdk.Organization) (string, error) {
var str strings.Builder
_, _ = fmt.Fprint(&str, "Organizations you are a member of:\n")
for _, org := range orgs {
_, _ = fmt.Fprintf(&str, "\t%s (%s)\n", org.Name, org.ID.String())
}
return str.String(), nil
}
orgs, err = client.OrganizationsByUser(inv.Context(), codersdk.Me)
if err != nil {
return err
}
default:
stringFormat = func(orgs []codersdk.Organization) (string, error) {
if len(orgs) != 1 {
return "", xerrors.Errorf("expected 1 organization, got %d", len(orgs))
}
return fmt.Sprintf("Organization: %s (%s)\n", orgs[0].Name, orgs[0].ID.String()), nil
}
// This works for a uuid or a name
org, err := client.OrganizationByName(inv.Context(), orgArg)
if err != nil {
return err
}
orgs = []codersdk.Organization{org}
}
if onlyID {
for _, org := range orgs {
_, _ = fmt.Fprintf(inv.Stdout, "%s\n", org.ID)
}
} else {
out, err := formatter.Format(inv.Context(), orgs)
if err != nil {
return err
}
_, _ = fmt.Fprint(inv.Stdout, out)
}
return nil
},
}
formatter.AttachOptions(&cmd.Options)
return cmd
}