chore: remove org context switcher in the cli (#13674)

* chore: remove org context switcher in the cli
This commit is contained in:
Steven Masley
2024-07-01 11:04:45 -10:00
committed by GitHub
parent 4a0fd7466c
commit 5bf46f360a
52 changed files with 353 additions and 362 deletions

View File

@ -29,6 +29,7 @@ func (r *RootCmd) create() *serpent.Command {
parameterFlags workspaceParameterFlags parameterFlags workspaceParameterFlags
autoUpdates string autoUpdates string
copyParametersFrom string copyParametersFrom string
orgContext = NewOrganizationContext()
) )
client := new(codersdk.Client) client := new(codersdk.Client)
cmd := &serpent.Command{ cmd := &serpent.Command{
@ -43,7 +44,7 @@ func (r *RootCmd) create() *serpent.Command {
), ),
Middleware: serpent.Chain(r.InitClient(client)), Middleware: serpent.Chain(r.InitClient(client)),
Handler: func(inv *serpent.Invocation) error { Handler: func(inv *serpent.Invocation) error {
organization, err := CurrentOrganization(r, inv, client) organization, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return err return err
} }
@ -269,6 +270,7 @@ func (r *RootCmd) create() *serpent.Command {
) )
cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...) cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...)
cmd.Options = append(cmd.Options, parameterFlags.cliParameterDefaults()...) cmd.Options = append(cmd.Options, parameterFlags.cliParameterDefaults()...)
orgContext.AttachOptions(cmd)
return cmd return cmd
} }

View File

@ -358,13 +358,6 @@ func (r *RootCmd) login() *serpent.Command {
return xerrors.Errorf("write server url: %w", err) return xerrors.Errorf("write server url: %w", err)
} }
// If the current organization cannot be fetched, then reset the organization context.
// Otherwise, organization cli commands will fail.
_, err = CurrentOrganization(r, inv, client)
if err != nil {
_ = config.Organization().Delete()
}
_, _ = fmt.Fprintf(inv.Stdout, Caret+"Welcome to Coder, %s! You're authenticated.\n", pretty.Sprint(cliui.DefaultStyles.Keyword, resp.Username)) _, _ = fmt.Fprintf(inv.Stdout, Caret+"Welcome to Coder, %s! You're authenticated.\n", pretty.Sprint(cliui.DefaultStyles.Keyword, resp.Username))
return nil return nil
}, },

View File

@ -5,11 +5,9 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os"
"runtime" "runtime"
"testing" "testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -424,29 +422,6 @@ func TestLogin(t *testing.T) {
require.NotEqual(t, client.SessionToken(), sessionFile) require.NotEqual(t, client.SessionToken(), sessionFile)
}) })
// Login should reset the configured organization if the user is not a member
t.Run("ResetOrganization", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
root, cfg := clitest.New(t, "login", client.URL.String(), "--token", client.SessionToken())
notRealOrg := uuid.NewString()
err := cfg.Organization().Write(notRealOrg)
require.NoError(t, err, "write bad org to config")
err = root.Run()
require.NoError(t, err)
sessionFile, err := cfg.Session().Read()
require.NoError(t, err)
require.NotEqual(t, client.SessionToken(), sessionFile)
// Organization config should be deleted since the org does not exist
selected, err := cfg.Organization().Read()
require.ErrorIs(t, err, os.ErrNotExist)
require.NotEqual(t, selected, notRealOrg)
})
t.Run("KeepOrganizationContext", func(t *testing.T) { t.Run("KeepOrganizationContext", func(t *testing.T) {
t.Parallel() t.Parallel()
client := coderdtest.New(t, nil) client := coderdtest.New(t, nil)

View File

@ -1,22 +1,19 @@
package cli package cli
import ( import (
"errors"
"fmt" "fmt"
"os"
"slices"
"strings" "strings"
"golang.org/x/xerrors" "golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/cliui" "github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/config"
"github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
"github.com/coder/serpent" "github.com/coder/serpent"
) )
func (r *RootCmd) organizations() *serpent.Command { func (r *RootCmd) organizations() *serpent.Command {
orgContext := NewOrganizationContext()
cmd := &serpent.Command{ cmd := &serpent.Command{
Use: "organizations [subcommand]", Use: "organizations [subcommand]",
Short: "Organization related commands", Short: "Organization related commands",
@ -26,188 +23,18 @@ func (r *RootCmd) organizations() *serpent.Command {
return inv.Command.HelpHandler(inv) return inv.Command.HelpHandler(inv)
}, },
Children: []*serpent.Command{ Children: []*serpent.Command{
r.currentOrganization(), r.showOrganization(orgContext),
r.switchOrganization(),
r.createOrganization(), r.createOrganization(),
r.organizationMembers(), r.organizationMembers(orgContext),
r.organizationRoles(), r.organizationRoles(orgContext),
}, },
} }
cmd.Options = serpent.OptionSet{} orgContext.AttachOptions(cmd)
return cmd return cmd
} }
func (r *RootCmd) switchOrganization() *serpent.Command { func (r *RootCmd) showOrganization(orgContext *OrganizationContext) *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 ( var (
stringFormat func(orgs []codersdk.Organization) (string, error) stringFormat func(orgs []codersdk.Organization) (string, error)
client = new(codersdk.Client) client = new(codersdk.Client)
@ -226,8 +53,29 @@ func (r *RootCmd) currentOrganization() *serpent.Command {
onlyID = false onlyID = false
) )
cmd := &serpent.Command{ cmd := &serpent.Command{
Use: "show [current|me|uuid]", Use: "show [\"selected\"|\"me\"|uuid|org_name]",
Short: "Show the organization, if no argument is given, the organization currently in use will be shown.", Short: "Show the organization. " +
"Using \"selected\" will show the selected organization from the \"--org\" flag. " +
"Using \"me\" will show all organizations you are a member of.",
Long: FormatExamples(
Example{
Description: "coder org show selected",
Command: "Shows the organizations selected with '--org=<org_name>'. " +
"This organization is the organization used by the cli.",
},
Example{
Description: "coder org show me",
Command: "List of all organizations you are a member of.",
},
Example{
Description: "coder org show developers",
Command: "Show organization with name 'developers'",
},
Example{
Description: "coder org show 90ee1875-3db5-43b3-828e-af3687522e43",
Command: "Show organization with the given ID.",
},
),
Middleware: serpent.Chain( Middleware: serpent.Chain(
r.InitClient(client), r.InitClient(client),
serpent.RequireRangeArgs(0, 1), serpent.RequireRangeArgs(0, 1),
@ -242,7 +90,7 @@ func (r *RootCmd) currentOrganization() *serpent.Command {
}, },
}, },
Handler: func(inv *serpent.Invocation) error { Handler: func(inv *serpent.Invocation) error {
orgArg := "current" orgArg := "selected"
if len(inv.Args) >= 1 { if len(inv.Args) >= 1 {
orgArg = inv.Args[0] orgArg = inv.Args[0]
} }
@ -250,14 +98,14 @@ func (r *RootCmd) currentOrganization() *serpent.Command {
var orgs []codersdk.Organization var orgs []codersdk.Organization
var err error var err error
switch strings.ToLower(orgArg) { switch strings.ToLower(orgArg) {
case "current": case "selected":
stringFormat = func(orgs []codersdk.Organization) (string, error) { stringFormat = func(orgs []codersdk.Organization) (string, error) {
if len(orgs) != 1 { if len(orgs) != 1 {
return "", xerrors.Errorf("expected 1 organization, got %d", len(orgs)) 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 return fmt.Sprintf("Current CLI Organization: %s (%s)\n", orgs[0].Name, orgs[0].ID.String()), nil
} }
org, err := CurrentOrganization(r, inv, client) org, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return err return err
} }

View File

@ -43,7 +43,7 @@ func TestCurrentOrganization(t *testing.T) {
defer srv.Close() defer srv.Close()
client := codersdk.New(must(url.Parse(srv.URL))) client := codersdk.New(must(url.Parse(srv.URL)))
inv, root := clitest.New(t, "organizations", "show", "current") inv, root := clitest.New(t, "organizations", "show", "selected")
clitest.SetupConfig(t, client, root) clitest.SetupConfig(t, client, root)
pty := ptytest.New(t).Attach(inv) pty := ptytest.New(t).Attach(inv)
errC := make(chan error) errC := make(chan error)
@ -70,7 +70,7 @@ func TestCurrentOrganization(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
} }
inv, root := clitest.New(t, "organizations", "show", "--only-id") inv, root := clitest.New(t, "organizations", "show", "--only-id", "--org="+first.OrganizationID.String())
clitest.SetupConfig(t, client, root) clitest.SetupConfig(t, client, root)
pty := ptytest.New(t).Attach(inv) pty := ptytest.New(t).Attach(inv)
errC := make(chan error) errC := make(chan error)
@ -101,7 +101,7 @@ func TestCurrentOrganization(t *testing.T) {
orgs[orgName] = org orgs[orgName] = org
} }
inv, root := clitest.New(t, "organizations", "show", "current", "--only-id", "-z=bar") inv, root := clitest.New(t, "organizations", "show", "selected", "--only-id", "-O=bar")
clitest.SetupConfig(t, client, root) clitest.SetupConfig(t, client, root)
pty := ptytest.New(t).Attach(inv) pty := ptytest.New(t).Attach(inv)
errC := make(chan error) errC := make(chan error)
@ -113,40 +113,6 @@ func TestCurrentOrganization(t *testing.T) {
}) })
} }
func TestOrganizationSwitch(t *testing.T) {
t.Parallel()
t.Run("Switch", func(t *testing.T) {
t.Parallel()
ownerClient := coderdtest.New(t, nil)
first := coderdtest.CreateFirstUser(t, ownerClient)
// Owner is required to make orgs
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, first.OrganizationID, rbac.RoleOwner())
ctx := testutil.Context(t, testutil.WaitMedium)
orgs := []string{"foo", "bar"}
for _, orgName := range orgs {
_, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
Name: orgName,
})
require.NoError(t, err)
}
exp, err := client.OrganizationByName(ctx, "foo")
require.NoError(t, err)
inv, root := clitest.New(t, "organizations", "set", "foo")
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t).Attach(inv)
errC := make(chan error)
go func() {
errC <- inv.Run()
}()
require.NoError(t, <-errC)
pty.ExpectMatch(exp.ID.String())
})
}
func must[V any](v V, err error) V { func must[V any](v V, err error) V {
if err != nil { if err != nil {
panic(err) panic(err)

View File

@ -11,16 +11,16 @@ import (
"github.com/coder/serpent" "github.com/coder/serpent"
) )
func (r *RootCmd) organizationMembers() *serpent.Command { func (r *RootCmd) organizationMembers(orgContext *OrganizationContext) *serpent.Command {
cmd := &serpent.Command{ cmd := &serpent.Command{
Use: "members", Use: "members",
Aliases: []string{"member"}, Aliases: []string{"member"},
Short: "Manage organization members", Short: "Manage organization members",
Children: []*serpent.Command{ Children: []*serpent.Command{
r.listOrganizationMembers(), r.listOrganizationMembers(orgContext),
r.assignOrganizationRoles(), r.assignOrganizationRoles(orgContext),
r.addOrganizationMember(), r.addOrganizationMember(orgContext),
r.removeOrganizationMember(), r.removeOrganizationMember(orgContext),
}, },
Handler: func(inv *serpent.Invocation) error { Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv) return inv.Command.HelpHandler(inv)
@ -30,7 +30,7 @@ func (r *RootCmd) organizationMembers() *serpent.Command {
return cmd return cmd
} }
func (r *RootCmd) removeOrganizationMember() *serpent.Command { func (r *RootCmd) removeOrganizationMember(orgContext *OrganizationContext) *serpent.Command {
client := new(codersdk.Client) client := new(codersdk.Client)
cmd := &serpent.Command{ cmd := &serpent.Command{
@ -42,7 +42,7 @@ func (r *RootCmd) removeOrganizationMember() *serpent.Command {
), ),
Handler: func(inv *serpent.Invocation) error { Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context() ctx := inv.Context()
organization, err := CurrentOrganization(r, inv, client) organization, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return err return err
} }
@ -61,7 +61,7 @@ func (r *RootCmd) removeOrganizationMember() *serpent.Command {
return cmd return cmd
} }
func (r *RootCmd) addOrganizationMember() *serpent.Command { func (r *RootCmd) addOrganizationMember(orgContext *OrganizationContext) *serpent.Command {
client := new(codersdk.Client) client := new(codersdk.Client)
cmd := &serpent.Command{ cmd := &serpent.Command{
@ -73,7 +73,7 @@ func (r *RootCmd) addOrganizationMember() *serpent.Command {
), ),
Handler: func(inv *serpent.Invocation) error { Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context() ctx := inv.Context()
organization, err := CurrentOrganization(r, inv, client) organization, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return err return err
} }
@ -92,7 +92,7 @@ func (r *RootCmd) addOrganizationMember() *serpent.Command {
return cmd return cmd
} }
func (r *RootCmd) assignOrganizationRoles() *serpent.Command { func (r *RootCmd) assignOrganizationRoles(orgContext *OrganizationContext) *serpent.Command {
client := new(codersdk.Client) client := new(codersdk.Client)
cmd := &serpent.Command{ cmd := &serpent.Command{
@ -104,7 +104,7 @@ func (r *RootCmd) assignOrganizationRoles() *serpent.Command {
), ),
Handler: func(inv *serpent.Invocation) error { Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context() ctx := inv.Context()
organization, err := CurrentOrganization(r, inv, client) organization, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return err return err
} }
@ -135,7 +135,7 @@ func (r *RootCmd) assignOrganizationRoles() *serpent.Command {
return cmd return cmd
} }
func (r *RootCmd) listOrganizationMembers() *serpent.Command { func (r *RootCmd) listOrganizationMembers(orgContext *OrganizationContext) *serpent.Command {
formatter := cliui.NewOutputFormatter( formatter := cliui.NewOutputFormatter(
cliui.TableFormat([]codersdk.OrganizationMemberWithName{}, []string{"username", "organization_roles"}), cliui.TableFormat([]codersdk.OrganizationMemberWithName{}, []string{"username", "organization_roles"}),
cliui.JSONFormat(), cliui.JSONFormat(),
@ -151,7 +151,7 @@ func (r *RootCmd) listOrganizationMembers() *serpent.Command {
), ),
Handler: func(inv *serpent.Invocation) error { Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context() ctx := inv.Context()
organization, err := CurrentOrganization(r, inv, client) organization, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return err return err
} }

View File

@ -56,7 +56,7 @@ func TestAddOrganizationMembers(t *testing.T) {
}) })
require.NoError(t, err, "create another organization") require.NoError(t, err, "create another organization")
inv, root := clitest.New(t, "organization", "members", "add", "--organization", otherOrg.ID.String(), user.Username) inv, root := clitest.New(t, "organization", "members", "add", "-O", otherOrg.ID.String(), user.Username)
//nolint:gocritic // must be an owner //nolint:gocritic // must be an owner
clitest.SetupConfig(t, ownerClient, root) clitest.SetupConfig(t, ownerClient, root)
@ -86,7 +86,7 @@ func TestRemoveOrganizationMembers(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitMedium) ctx := testutil.Context(t, testutil.WaitMedium)
inv, root := clitest.New(t, "organization", "members", "remove", "--organization", owner.OrganizationID.String(), user.Username) inv, root := clitest.New(t, "organization", "members", "remove", "-O", owner.OrganizationID.String(), user.Username)
clitest.SetupConfig(t, orgAdminClient, root) clitest.SetupConfig(t, orgAdminClient, root)
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
@ -109,7 +109,7 @@ func TestRemoveOrganizationMembers(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitMedium) ctx := testutil.Context(t, testutil.WaitMedium)
inv, root := clitest.New(t, "organization", "members", "remove", "--organization", owner.OrganizationID.String(), "random_name") inv, root := clitest.New(t, "organization", "members", "remove", "-O", owner.OrganizationID.String(), "random_name")
clitest.SetupConfig(t, orgAdminClient, root) clitest.SetupConfig(t, orgAdminClient, root)
buf := new(bytes.Buffer) buf := new(bytes.Buffer)

View File

@ -16,7 +16,7 @@ import (
"github.com/coder/serpent" "github.com/coder/serpent"
) )
func (r *RootCmd) organizationRoles() *serpent.Command { func (r *RootCmd) organizationRoles(orgContext *OrganizationContext) *serpent.Command {
cmd := &serpent.Command{ cmd := &serpent.Command{
Use: "roles", Use: "roles",
Short: "Manage organization roles.", Short: "Manage organization roles.",
@ -26,14 +26,14 @@ func (r *RootCmd) organizationRoles() *serpent.Command {
}, },
Hidden: true, Hidden: true,
Children: []*serpent.Command{ Children: []*serpent.Command{
r.showOrganizationRoles(), r.showOrganizationRoles(orgContext),
r.editOrganizationRole(), r.editOrganizationRole(orgContext),
}, },
} }
return cmd return cmd
} }
func (r *RootCmd) showOrganizationRoles() *serpent.Command { func (r *RootCmd) showOrganizationRoles(orgContext *OrganizationContext) *serpent.Command {
formatter := cliui.NewOutputFormatter( formatter := cliui.NewOutputFormatter(
cliui.ChangeFormatterData( cliui.ChangeFormatterData(
cliui.TableFormat([]roleTableRow{}, []string{"name", "display_name", "site_permissions", "organization_permissions", "user_permissions"}), cliui.TableFormat([]roleTableRow{}, []string{"name", "display_name", "site_permissions", "organization_permissions", "user_permissions"}),
@ -63,7 +63,7 @@ func (r *RootCmd) showOrganizationRoles() *serpent.Command {
), ),
Handler: func(inv *serpent.Invocation) error { Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context() ctx := inv.Context()
org, err := CurrentOrganization(r, inv, client) org, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return err return err
} }
@ -100,7 +100,7 @@ func (r *RootCmd) showOrganizationRoles() *serpent.Command {
return cmd return cmd
} }
func (r *RootCmd) editOrganizationRole() *serpent.Command { func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent.Command {
formatter := cliui.NewOutputFormatter( formatter := cliui.NewOutputFormatter(
cliui.ChangeFormatterData( cliui.ChangeFormatterData(
cliui.TableFormat([]roleTableRow{}, []string{"name", "display_name", "site_permissions", "organization_permissions", "user_permissions"}), cliui.TableFormat([]roleTableRow{}, []string{"name", "display_name", "site_permissions", "organization_permissions", "user_permissions"}),
@ -148,7 +148,7 @@ func (r *RootCmd) editOrganizationRole() *serpent.Command {
), ),
Handler: func(inv *serpent.Invocation) error { Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context() ctx := inv.Context()
org, err := CurrentOrganization(r, inv, client) org, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return err return err
} }

View File

@ -29,6 +29,7 @@ import (
"golang.org/x/mod/semver" "golang.org/x/mod/semver"
"golang.org/x/xerrors" "golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/pretty" "github.com/coder/pretty"
"github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/buildinfo"
@ -52,20 +53,19 @@ var (
) )
const ( const (
varURL = "url" varURL = "url"
varToken = "token" varToken = "token"
varAgentToken = "agent-token" varAgentToken = "agent-token"
varAgentTokenFile = "agent-token-file" varAgentTokenFile = "agent-token-file"
varAgentURL = "agent-url" varAgentURL = "agent-url"
varHeader = "header" varHeader = "header"
varHeaderCommand = "header-command" varHeaderCommand = "header-command"
varNoOpen = "no-open" varNoOpen = "no-open"
varNoVersionCheck = "no-version-warning" varNoVersionCheck = "no-version-warning"
varNoFeatureWarning = "no-feature-warning" varNoFeatureWarning = "no-feature-warning"
varForceTty = "force-tty" varForceTty = "force-tty"
varVerbose = "verbose" varVerbose = "verbose"
varOrganizationSelect = "organization" varDisableDirect = "disable-direct-connections"
varDisableDirect = "disable-direct-connections"
notLoggedInMessage = "You are not logged in. Try logging in using 'coder login <url>'." notLoggedInMessage = "You are not logged in. Try logging in using 'coder login <url>'."
@ -451,15 +451,6 @@ func (r *RootCmd) Command(subcommands []*serpent.Command) (*serpent.Command, err
Value: serpent.StringOf(&r.globalConfig), Value: serpent.StringOf(&r.globalConfig),
Group: globalGroup, Group: globalGroup,
}, },
{
Flag: varOrganizationSelect,
FlagShorthand: "z",
Env: "CODER_ORGANIZATION",
Description: "Select which organization (uuid or name) to use This overrides what is present in the config file.",
Value: serpent.StringOf(&r.organizationSelect),
Hidden: true,
Group: globalGroup,
},
{ {
Flag: "version", Flag: "version",
// This was requested by a customer to assist with their migration. // This was requested by a customer to assist with their migration.
@ -476,21 +467,20 @@ func (r *RootCmd) Command(subcommands []*serpent.Command) (*serpent.Command, err
// RootCmd contains parameters and helpers useful to all commands. // RootCmd contains parameters and helpers useful to all commands.
type RootCmd struct { type RootCmd struct {
clientURL *url.URL clientURL *url.URL
token string token string
globalConfig string globalConfig string
header []string header []string
headerCommand string headerCommand string
agentToken string agentToken string
agentTokenFile string agentTokenFile string
agentURL *url.URL agentURL *url.URL
forceTTY bool forceTTY bool
noOpen bool noOpen bool
verbose bool verbose bool
organizationSelect string versionFlag bool
versionFlag bool disableDirect bool
disableDirect bool debugHTTP bool
debugHTTP bool
noVersionCheck bool noVersionCheck bool
noFeatureWarning bool noFeatureWarning bool
@ -632,52 +622,58 @@ func (r *RootCmd) createAgentClient() (*agentsdk.Client, error) {
return client, nil return client, nil
} }
// CurrentOrganization returns the currently active organization for the authenticated user. type OrganizationContext struct {
func CurrentOrganization(r *RootCmd, inv *serpent.Invocation, client *codersdk.Client) (codersdk.Organization, error) { // FlagSelect is the value passed in via the --org flag
conf := r.createConfig() FlagSelect string
selected := r.organizationSelect }
if selected == "" && conf.Organization().Exists() {
org, err := conf.Organization().Read()
if err != nil {
return codersdk.Organization{}, xerrors.Errorf("read selected organization from config file %q: %w", conf.Organization(), err)
}
selected = org
}
// Verify the org exists and the user is a member func NewOrganizationContext() *OrganizationContext {
return &OrganizationContext{}
}
func (o *OrganizationContext) AttachOptions(cmd *serpent.Command) {
cmd.Options = append(cmd.Options, serpent.Option{
Name: "Organization",
Description: "Select which organization (uuid or name) to use.",
// Only required if the user is a part of more than 1 organization.
// Otherwise, we can assume a default value.
Required: false,
Flag: "org",
FlagShorthand: "O",
Env: "CODER_ORGANIZATION",
Value: serpent.StringOf(&o.FlagSelect),
})
}
func (o *OrganizationContext) Selected(inv *serpent.Invocation, client *codersdk.Client) (codersdk.Organization, error) {
// Fetch the set of organizations the user is a member of.
orgs, err := client.OrganizationsByUser(inv.Context(), codersdk.Me) orgs, err := client.OrganizationsByUser(inv.Context(), codersdk.Me)
if err != nil { if err != nil {
return codersdk.Organization{}, err return codersdk.Organization{}, xerrors.Errorf("get organizations: %w", err)
} }
// User manually selected an organization // User manually selected an organization
if selected != "" { if o.FlagSelect != "" {
index := slices.IndexFunc(orgs, func(org codersdk.Organization) bool { index := slices.IndexFunc(orgs, func(org codersdk.Organization) bool {
return org.Name == selected || org.ID.String() == selected return org.Name == o.FlagSelect || org.ID.String() == o.FlagSelect
}) })
if index < 0 { if index < 0 {
return codersdk.Organization{}, xerrors.Errorf("organization %q not found, are you sure you are a member of this organization? If unsure, run 'coder organizations set \"\" ' to reset your current context.", selected) names := db2sdk.List(orgs, func(f codersdk.Organization) string {
return f.Name
})
return codersdk.Organization{}, xerrors.Errorf("organization %q not found, are you sure you are a member of this organization? "+
"Valid options for '--org=' are [%s].", o.FlagSelect, strings.Join(names, ", "))
} }
return orgs[index], nil return orgs[index], nil
} }
// User did not select an organization, so use the default. if len(orgs) == 1 {
index := slices.IndexFunc(orgs, func(org codersdk.Organization) bool { return orgs[0], nil
return org.IsDefault
})
if index < 0 {
if len(orgs) == 1 {
// If there is no "isDefault", but only 1 org is present. We can just
// assume the single organization is correct. This is mainly a helper
// for cli hitting an old instance, or a user that belongs to a single
// org that is not the default.
return orgs[0], nil
}
return codersdk.Organization{}, xerrors.Errorf("unable to determine current organization. Use 'coder org set <org>' to select an organization to use")
} }
return orgs[index], nil // No org selected, and we are more than 1? Return an error.
return codersdk.Organization{}, xerrors.Errorf("Must select an organization with --org=<org_name>.")
} }
func splitNamedWorkspace(identifier string) (owner string, workspaceName string, err error) { func splitNamedWorkspace(identifier string) (owner string, workspaceName string, err error) {

View File

@ -31,6 +31,7 @@ func (r *RootCmd) templateCreate() *serpent.Command {
dormancyAutoDeletion time.Duration dormancyAutoDeletion time.Duration
uploadFlags templateUploadFlags uploadFlags templateUploadFlags
orgContext = NewOrganizationContext()
) )
client := new(codersdk.Client) client := new(codersdk.Client)
cmd := &serpent.Command{ cmd := &serpent.Command{
@ -68,7 +69,7 @@ func (r *RootCmd) templateCreate() *serpent.Command {
} }
} }
organization, err := CurrentOrganization(r, inv, client) organization, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return err return err
} }

View File

@ -15,6 +15,7 @@ import (
) )
func (r *RootCmd) templateDelete() *serpent.Command { func (r *RootCmd) templateDelete() *serpent.Command {
orgContext := NewOrganizationContext()
client := new(codersdk.Client) client := new(codersdk.Client)
cmd := &serpent.Command{ cmd := &serpent.Command{
Use: "delete [name...]", Use: "delete [name...]",
@ -32,7 +33,7 @@ func (r *RootCmd) templateDelete() *serpent.Command {
templates = []codersdk.Template{} templates = []codersdk.Template{}
) )
organization, err := CurrentOrganization(r, inv, client) organization, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return err return err
} }
@ -81,6 +82,7 @@ func (r *RootCmd) templateDelete() *serpent.Command {
return nil return nil
}, },
} }
orgContext.AttachOptions(cmd)
return cmd return cmd
} }

View File

@ -36,6 +36,7 @@ func (r *RootCmd) templateEdit() *serpent.Command {
requireActiveVersion bool requireActiveVersion bool
deprecationMessage string deprecationMessage string
disableEveryone bool disableEveryone bool
orgContext = NewOrganizationContext()
) )
client := new(codersdk.Client) client := new(codersdk.Client)
@ -77,7 +78,7 @@ func (r *RootCmd) templateEdit() *serpent.Command {
} }
} }
organization, err := CurrentOrganization(r, inv, client) organization, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return xerrors.Errorf("get current organization: %w", err) return xerrors.Errorf("get current organization: %w", err)
} }
@ -324,6 +325,7 @@ func (r *RootCmd) templateEdit() *serpent.Command {
}, },
cliui.SkipPromptOption(), cliui.SkipPromptOption(),
} }
orgContext.AttachOptions(cmd)
return cmd return cmd
} }

View File

@ -11,6 +11,7 @@ import (
) )
func (r *RootCmd) templateList() *serpent.Command { func (r *RootCmd) templateList() *serpent.Command {
orgContext := NewOrganizationContext()
formatter := cliui.NewOutputFormatter( formatter := cliui.NewOutputFormatter(
cliui.TableFormat([]templateTableRow{}, []string{"name", "last updated", "used by"}), cliui.TableFormat([]templateTableRow{}, []string{"name", "last updated", "used by"}),
cliui.JSONFormat(), cliui.JSONFormat(),
@ -25,7 +26,7 @@ func (r *RootCmd) templateList() *serpent.Command {
r.InitClient(client), r.InitClient(client),
), ),
Handler: func(inv *serpent.Invocation) error { Handler: func(inv *serpent.Invocation) error {
organization, err := CurrentOrganization(r, inv, client) organization, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return err return err
} }
@ -52,5 +53,6 @@ func (r *RootCmd) templateList() *serpent.Command {
} }
formatter.AttachOptions(&cmd.Options) formatter.AttachOptions(&cmd.Options)
orgContext.AttachOptions(cmd)
return cmd return cmd
} }

View File

@ -20,6 +20,7 @@ func (r *RootCmd) templatePull() *serpent.Command {
tarMode bool tarMode bool
zipMode bool zipMode bool
versionName string versionName string
orgContext = NewOrganizationContext()
) )
client := new(codersdk.Client) client := new(codersdk.Client)
@ -45,7 +46,7 @@ func (r *RootCmd) templatePull() *serpent.Command {
return xerrors.Errorf("either tar or zip can be selected") return xerrors.Errorf("either tar or zip can be selected")
} }
organization, err := CurrentOrganization(r, inv, client) organization, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return xerrors.Errorf("get current organization: %w", err) return xerrors.Errorf("get current organization: %w", err)
} }
@ -187,6 +188,7 @@ func (r *RootCmd) templatePull() *serpent.Command {
}, },
cliui.SkipPromptOption(), cliui.SkipPromptOption(),
} }
orgContext.AttachOptions(cmd)
return cmd return cmd
} }

View File

@ -34,6 +34,7 @@ func (r *RootCmd) templatePush() *serpent.Command {
provisionerTags []string provisionerTags []string
uploadFlags templateUploadFlags uploadFlags templateUploadFlags
activate bool activate bool
orgContext = NewOrganizationContext()
) )
client := new(codersdk.Client) client := new(codersdk.Client)
cmd := &serpent.Command{ cmd := &serpent.Command{
@ -46,7 +47,7 @@ func (r *RootCmd) templatePush() *serpent.Command {
Handler: func(inv *serpent.Invocation) error { Handler: func(inv *serpent.Invocation) error {
uploadFlags.setWorkdir(workdir) uploadFlags.setWorkdir(workdir)
organization, err := CurrentOrganization(r, inv, client) organization, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return err return err
} }
@ -226,6 +227,7 @@ func (r *RootCmd) templatePush() *serpent.Command {
cliui.SkipPromptOption(), cliui.SkipPromptOption(),
} }
cmd.Options = append(cmd.Options, uploadFlags.options()...) cmd.Options = append(cmd.Options, uploadFlags.options()...)
orgContext.AttachOptions(cmd)
return cmd return cmd
} }

View File

@ -31,6 +31,7 @@ func (r *RootCmd) setArchiveTemplateVersion(archive bool) *serpent.Command {
pastVerb = "unarchived" pastVerb = "unarchived"
} }
orgContext := NewOrganizationContext()
client := new(codersdk.Client) client := new(codersdk.Client)
cmd := &serpent.Command{ cmd := &serpent.Command{
Use: presentVerb + " <template-name> [template-version-names...] ", Use: presentVerb + " <template-name> [template-version-names...] ",
@ -47,7 +48,7 @@ func (r *RootCmd) setArchiveTemplateVersion(archive bool) *serpent.Command {
versions []codersdk.TemplateVersion versions []codersdk.TemplateVersion
) )
organization, err := CurrentOrganization(r, inv, client) organization, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return err return err
} }
@ -92,6 +93,7 @@ func (r *RootCmd) setArchiveTemplateVersion(archive bool) *serpent.Command {
return nil return nil
}, },
} }
orgContext.AttachOptions(cmd)
return cmd return cmd
} }
@ -99,6 +101,7 @@ func (r *RootCmd) setArchiveTemplateVersion(archive bool) *serpent.Command {
func (r *RootCmd) archiveTemplateVersions() *serpent.Command { func (r *RootCmd) archiveTemplateVersions() *serpent.Command {
var all serpent.Bool var all serpent.Bool
client := new(codersdk.Client) client := new(codersdk.Client)
orgContext := NewOrganizationContext()
cmd := &serpent.Command{ cmd := &serpent.Command{
Use: "archive [template-name...] ", Use: "archive [template-name...] ",
Short: "Archive unused or failed template versions from a given template(s)", Short: "Archive unused or failed template versions from a given template(s)",
@ -121,7 +124,7 @@ func (r *RootCmd) archiveTemplateVersions() *serpent.Command {
templates = []codersdk.Template{} templates = []codersdk.Template{}
) )
organization, err := CurrentOrganization(r, inv, client) organization, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return err return err
} }
@ -179,6 +182,7 @@ func (r *RootCmd) archiveTemplateVersions() *serpent.Command {
return nil return nil
}, },
} }
orgContext.AttachOptions(cmd)
return cmd return cmd
} }

View File

@ -51,6 +51,7 @@ func (r *RootCmd) templateVersionsList() *serpent.Command {
cliui.JSONFormat(), cliui.JSONFormat(),
) )
client := new(codersdk.Client) client := new(codersdk.Client)
orgContext := NewOrganizationContext()
var includeArchived serpent.Bool var includeArchived serpent.Bool
@ -93,7 +94,7 @@ func (r *RootCmd) templateVersionsList() *serpent.Command {
}, },
}, },
Handler: func(inv *serpent.Invocation) error { Handler: func(inv *serpent.Invocation) error {
organization, err := CurrentOrganization(r, inv, client) organization, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return xerrors.Errorf("get current organization: %w", err) return xerrors.Errorf("get current organization: %w", err)
} }
@ -122,6 +123,7 @@ func (r *RootCmd) templateVersionsList() *serpent.Command {
}, },
} }
orgContext.AttachOptions(cmd)
formatter.AttachOptions(&cmd.Options) formatter.AttachOptions(&cmd.Options)
return cmd return cmd
} }

View File

@ -10,6 +10,9 @@ USAGE:
$ coder create <username>/<workspace_name> $ coder create <username>/<workspace_name>
OPTIONS: OPTIONS:
-O, --org string, $CODER_ORGANIZATION
Select which organization (uuid or name) to use.
--automatic-updates string, $CODER_WORKSPACE_AUTOMATIC_UPDATES (default: never) --automatic-updates string, $CODER_WORKSPACE_AUTOMATIC_UPDATES (default: never)
Specify automatic updates setting for the workspace (accepts 'always' Specify automatic updates setting for the workspace (accepts 'always'
or 'never'). or 'never').

View File

@ -6,6 +6,9 @@ USAGE:
Archive unused or failed template versions from a given template(s) Archive unused or failed template versions from a given template(s)
OPTIONS: OPTIONS:
-O, --org string, $CODER_ORGANIZATION
Select which organization (uuid or name) to use.
--all bool --all bool
Include all unused template versions. By default, only failed template Include all unused template versions. By default, only failed template
versions are archived. versions are archived.

View File

@ -8,6 +8,9 @@ USAGE:
Aliases: rm Aliases: rm
OPTIONS: OPTIONS:
-O, --org string, $CODER_ORGANIZATION
Select which organization (uuid or name) to use.
-y, --yes bool -y, --yes bool
Bypass prompts. Bypass prompts.

View File

@ -6,6 +6,9 @@ USAGE:
Edit the metadata of a template by name. Edit the metadata of a template by name.
OPTIONS: OPTIONS:
-O, --org string, $CODER_ORGANIZATION
Select which organization (uuid or name) to use.
--activity-bump duration --activity-bump duration
Edit the template activity bump - workspaces created from this Edit the template activity bump - workspaces created from this
template will have their shutdown time bumped by this value when template will have their shutdown time bumped by this value when

View File

@ -8,6 +8,9 @@ USAGE:
Aliases: ls Aliases: ls
OPTIONS: OPTIONS:
-O, --org string, $CODER_ORGANIZATION
Select which organization (uuid or name) to use.
-c, --column string-array (default: name,last updated,used by) -c, --column string-array (default: name,last updated,used by)
Columns to display in table output. Available columns: name, created Columns to display in table output. Available columns: name, created
at, last updated, organization id, provisioner, active version id, at, last updated, organization id, provisioner, active version id,

View File

@ -6,6 +6,9 @@ USAGE:
Download the active, latest, or specified version of a template to a path. Download the active, latest, or specified version of a template to a path.
OPTIONS: OPTIONS:
-O, --org string, $CODER_ORGANIZATION
Select which organization (uuid or name) to use.
--tar bool --tar bool
Output the template as a tar archive to stdout. Output the template as a tar archive to stdout.

View File

@ -6,6 +6,9 @@ USAGE:
Create or update a template from the current directory or as specified by flag Create or update a template from the current directory or as specified by flag
OPTIONS: OPTIONS:
-O, --org string, $CODER_ORGANIZATION
Select which organization (uuid or name) to use.
--activate bool (default: true) --activate bool (default: true)
Whether the new template will be marked active. Whether the new template will be marked active.

View File

@ -7,6 +7,9 @@ USAGE:
Archive a template version(s). Archive a template version(s).
OPTIONS: OPTIONS:
-O, --org string, $CODER_ORGANIZATION
Select which organization (uuid or name) to use.
-y, --yes bool -y, --yes bool
Bypass prompts. Bypass prompts.

View File

@ -6,6 +6,9 @@ USAGE:
List all the versions of the specified template List all the versions of the specified template
OPTIONS: OPTIONS:
-O, --org string, $CODER_ORGANIZATION
Select which organization (uuid or name) to use.
-c, --column string-array (default: Name,Created At,Created By,Status,Active) -c, --column string-array (default: Name,Created At,Created By,Status,Active)
Columns to display in table output. Available columns: name, created Columns to display in table output. Available columns: name, created
at, created by, status, active, archived. at, created by, status, active, archived.

View File

@ -7,6 +7,9 @@ USAGE:
Unarchive a template version(s). Unarchive a template version(s).
OPTIONS: OPTIONS:
-O, --org string, $CODER_ORGANIZATION
Select which organization (uuid or name) to use.
-y, --yes bool -y, --yes bool
Bypass prompts. Bypass prompts.

View File

@ -4,6 +4,9 @@ USAGE:
coder users create [flags] coder users create [flags]
OPTIONS: OPTIONS:
-O, --org string, $CODER_ORGANIZATION
Select which organization (uuid or name) to use.
-e, --email string -e, --email string
Specifies an email address for the new user. Specifies an email address for the new user.

View File

@ -24,6 +24,7 @@ func (r *RootCmd) userCreate() *serpent.Command {
password string password string
disableLogin bool disableLogin bool
loginType string loginType string
orgContext = NewOrganizationContext()
) )
client := new(codersdk.Client) client := new(codersdk.Client)
cmd := &serpent.Command{ cmd := &serpent.Command{
@ -33,7 +34,7 @@ func (r *RootCmd) userCreate() *serpent.Command {
r.InitClient(client), r.InitClient(client),
), ),
Handler: func(inv *serpent.Invocation) error { Handler: func(inv *serpent.Invocation) error {
organization, err := CurrentOrganization(r, inv, client) organization, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return err return err
} }
@ -175,5 +176,7 @@ Create a workspace `+pretty.Sprint(cliui.DefaultStyles.Code, "coder create")+`!
Value: serpent.StringOf(&loginType), Value: serpent.StringOf(&loginType),
}, },
} }
orgContext.AttachOptions(cmd)
return cmd return cmd
} }

9
docs/cli/create.md generated
View File

@ -100,3 +100,12 @@ Specify a file path with values for rich parameters defined in the template.
| Environment | <code>$CODER_RICH_PARAMETER_DEFAULT</code> | | Environment | <code>$CODER_RICH_PARAMETER_DEFAULT</code> |
Rich parameter default values in the format "name=value". Rich parameter default values in the format "name=value".
### -O, --org
| | |
| ----------- | -------------------------------- |
| Type | <code>string</code> |
| Environment | <code>$CODER_ORGANIZATION</code> |
Select which organization (uuid or name) to use.

View File

@ -29,3 +29,12 @@ Set an avatar for a group.
| Environment | <code>$CODER_DISPLAY_NAME</code> | | Environment | <code>$CODER_DISPLAY_NAME</code> |
Optional human friendly name for the group. Optional human friendly name for the group.
### -O, --org
| | |
| ----------- | -------------------------------- |
| Type | <code>string</code> |
| Environment | <code>$CODER_ORGANIZATION</code> |
Select which organization (uuid or name) to use.

View File

@ -11,5 +11,16 @@ Aliases:
## Usage ## Usage
```console ```console
coder groups delete <name> coder groups delete [flags] <name>
``` ```
## Options
### -O, --org
| | |
| ----------- | -------------------------------- |
| Type | <code>string</code> |
| Environment | <code>$CODER_ORGANIZATION</code> |
Select which organization (uuid or name) to use.

View File

@ -52,3 +52,12 @@ Add users to the group. Accepts emails or IDs.
| Type | <code>string-array</code> | | Type | <code>string-array</code> |
Remove users to the group. Accepts emails or IDs. Remove users to the group. Accepts emails or IDs.
### -O, --org
| | |
| ----------- | -------------------------------- |
| Type | <code>string</code> |
| Environment | <code>$CODER_ORGANIZATION</code> |
Select which organization (uuid or name) to use.

View File

@ -29,3 +29,12 @@ Columns to display in table output. Available columns: name, display name, organ
| Default | <code>table</code> | | Default | <code>table</code> |
Output format. Available formats: table, json. Output format. Available formats: table, json.
### -O, --org
| | |
| ----------- | -------------------------------- |
| Type | <code>string</code> |
| Environment | <code>$CODER_ORGANIZATION</code> |
Select which organization (uuid or name) to use.

View File

@ -27,3 +27,12 @@ Bypass prompts.
| Type | <code>bool</code> | | Type | <code>bool</code> |
Include all unused template versions. By default, only failed template versions are archived. Include all unused template versions. By default, only failed template versions are archived.
### -O, --org
| | |
| ----------- | -------------------------------- |
| Type | <code>string</code> |
| Environment | <code>$CODER_ORGANIZATION</code> |
Select which organization (uuid or name) to use.

View File

@ -23,3 +23,12 @@ coder templates delete [flags] [name...]
| Type | <code>bool</code> | | Type | <code>bool</code> |
Bypass prompts. Bypass prompts.
### -O, --org
| | |
| ----------- | -------------------------------- |
| Type | <code>string</code> |
| Environment | <code>$CODER_ORGANIZATION</code> |
Select which organization (uuid or name) to use.

View File

@ -171,3 +171,12 @@ Disable the default behavior of granting template access to the 'everyone' group
| Type | <code>bool</code> | | Type | <code>bool</code> |
Bypass prompts. Bypass prompts.
### -O, --org
| | |
| ----------- | -------------------------------- |
| Type | <code>string</code> |
| Environment | <code>$CODER_ORGANIZATION</code> |
Select which organization (uuid or name) to use.

View File

@ -33,3 +33,12 @@ Columns to display in table output. Available columns: name, created at, last up
| Default | <code>table</code> | | Default | <code>table</code> |
Output format. Available formats: table, json. Output format. Available formats: table, json.
### -O, --org
| | |
| ----------- | -------------------------------- |
| Type | <code>string</code> |
| Environment | <code>$CODER_ORGANIZATION</code> |
Select which organization (uuid or name) to use.

View File

@ -43,3 +43,12 @@ The name of the template version to pull. Use 'active' to pull the active versio
| Type | <code>bool</code> | | Type | <code>bool</code> |
Bypass prompts. Bypass prompts.
### -O, --org
| | |
| ----------- | -------------------------------- |
| Type | <code>string</code> |
| Environment | <code>$CODER_ORGANIZATION</code> |
Select which organization (uuid or name) to use.

View File

@ -102,3 +102,12 @@ Ignore warnings about not having a .terraform.lock.hcl file present in the templ
| Type | <code>string</code> | | Type | <code>string</code> |
Specify a message describing the changes in this version of the template. Messages longer than 72 characters will be displayed as truncated. Specify a message describing the changes in this version of the template. Messages longer than 72 characters will be displayed as truncated.
### -O, --org
| | |
| ----------- | -------------------------------- |
| Type | <code>string</code> |
| Environment | <code>$CODER_ORGANIZATION</code> |
Select which organization (uuid or name) to use.

View File

@ -19,3 +19,12 @@ coder templates versions archive [flags] <template-name> [template-version-names
| Type | <code>bool</code> | | Type | <code>bool</code> |
Bypass prompts. Bypass prompts.
### -O, --org
| | |
| ----------- | -------------------------------- |
| Type | <code>string</code> |
| Environment | <code>$CODER_ORGANIZATION</code> |
Select which organization (uuid or name) to use.

View File

@ -20,6 +20,15 @@ coder templates versions list [flags] <template>
Include archived versions in the result list. Include archived versions in the result list.
### -O, --org
| | |
| ----------- | -------------------------------- |
| Type | <code>string</code> |
| Environment | <code>$CODER_ORGANIZATION</code> |
Select which organization (uuid or name) to use.
### -c, --column ### -c, --column
| | | | | |

View File

@ -19,3 +19,12 @@ coder templates versions unarchive [flags] <template-name> [template-version-nam
| Type | <code>bool</code> | | Type | <code>bool</code> |
Bypass prompts. Bypass prompts.
### -O, --org
| | |
| ----------- | -------------------------------- |
| Type | <code>string</code> |
| Environment | <code>$CODER_ORGANIZATION</code> |
Select which organization (uuid or name) to use.

View File

@ -49,3 +49,12 @@ Specifies a password for the new user.
| Type | <code>string</code> | | Type | <code>string</code> |
Optionally specify the login type for the user. Valid values are: password, none, github, oidc. Using 'none' prevents the user from authenticating and requires an API key/token to be generated by an admin. Optionally specify the login type for the user. Valid values are: password, none, github, oidc. Using 'none' prevents the user from authenticating and requires an API key/token to be generated by an admin.
### -O, --org
| | |
| ----------- | -------------------------------- |
| Type | <code>string</code> |
| Environment | <code>$CODER_ORGANIZATION</code> |
Select which organization (uuid or name) to use.

View File

@ -16,6 +16,7 @@ func (r *RootCmd) groupCreate() *serpent.Command {
var ( var (
avatarURL string avatarURL string
displayName string displayName string
orgContext = agpl.NewOrganizationContext()
) )
client := new(codersdk.Client) client := new(codersdk.Client)
@ -29,7 +30,7 @@ func (r *RootCmd) groupCreate() *serpent.Command {
Handler: func(inv *serpent.Invocation) error { Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context() ctx := inv.Context()
org, err := agpl.CurrentOrganization(&r.RootCmd, inv, client) org, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return xerrors.Errorf("current organization: %w", err) return xerrors.Errorf("current organization: %w", err)
} }
@ -63,6 +64,7 @@ func (r *RootCmd) groupCreate() *serpent.Command {
Value: serpent.StringOf(&displayName), Value: serpent.StringOf(&displayName),
}, },
} }
orgContext.AttachOptions(cmd)
return cmd return cmd
} }

View File

@ -13,6 +13,7 @@ import (
) )
func (r *RootCmd) groupDelete() *serpent.Command { func (r *RootCmd) groupDelete() *serpent.Command {
orgContext := agpl.NewOrganizationContext()
client := new(codersdk.Client) client := new(codersdk.Client)
cmd := &serpent.Command{ cmd := &serpent.Command{
Use: "delete <name>", Use: "delete <name>",
@ -27,7 +28,7 @@ func (r *RootCmd) groupDelete() *serpent.Command {
groupName = inv.Args[0] groupName = inv.Args[0]
) )
org, err := agpl.CurrentOrganization(&r.RootCmd, inv, client) org, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return xerrors.Errorf("current organization: %w", err) return xerrors.Errorf("current organization: %w", err)
} }
@ -46,6 +47,7 @@ func (r *RootCmd) groupDelete() *serpent.Command {
return nil return nil
}, },
} }
orgContext.AttachOptions(cmd)
return cmd return cmd
} }

View File

@ -22,6 +22,7 @@ func (r *RootCmd) groupEdit() *serpent.Command {
displayName string displayName string
addUsers []string addUsers []string
rmUsers []string rmUsers []string
orgContext = agpl.NewOrganizationContext()
) )
client := new(codersdk.Client) client := new(codersdk.Client)
cmd := &serpent.Command{ cmd := &serpent.Command{
@ -37,7 +38,7 @@ func (r *RootCmd) groupEdit() *serpent.Command {
groupName = inv.Args[0] groupName = inv.Args[0]
) )
org, err := agpl.CurrentOrganization(&r.RootCmd, inv, client) org, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return xerrors.Errorf("current organization: %w", err) return xerrors.Errorf("current organization: %w", err)
} }
@ -116,6 +117,7 @@ func (r *RootCmd) groupEdit() *serpent.Command {
Value: serpent.StringArrayOf(&rmUsers), Value: serpent.StringArrayOf(&rmUsers),
}, },
} }
orgContext.AttachOptions(cmd)
return cmd return cmd
} }

View File

@ -18,6 +18,7 @@ func (r *RootCmd) groupList() *serpent.Command {
cliui.TableFormat([]groupTableRow{}, nil), cliui.TableFormat([]groupTableRow{}, nil),
cliui.JSONFormat(), cliui.JSONFormat(),
) )
orgContext := agpl.NewOrganizationContext()
client := new(codersdk.Client) client := new(codersdk.Client)
cmd := &serpent.Command{ cmd := &serpent.Command{
@ -30,7 +31,7 @@ func (r *RootCmd) groupList() *serpent.Command {
Handler: func(inv *serpent.Invocation) error { Handler: func(inv *serpent.Invocation) error {
ctx := inv.Context() ctx := inv.Context()
org, err := agpl.CurrentOrganization(&r.RootCmd, inv, client) org, err := orgContext.Selected(inv, client)
if err != nil { if err != nil {
return xerrors.Errorf("current organization: %w", err) return xerrors.Errorf("current organization: %w", err)
} }
@ -58,6 +59,7 @@ func (r *RootCmd) groupList() *serpent.Command {
} }
formatter.AttachOptions(&cmd.Options) formatter.AttachOptions(&cmd.Options)
orgContext.AttachOptions(cmd)
return cmd return cmd
} }

View File

@ -6,6 +6,9 @@ USAGE:
Create a user group Create a user group
OPTIONS: OPTIONS:
-O, --org string, $CODER_ORGANIZATION
Select which organization (uuid or name) to use.
-u, --avatar-url string, $CODER_AVATAR_URL -u, --avatar-url string, $CODER_AVATAR_URL
Set an avatar for a group. Set an avatar for a group.

View File

@ -1,11 +1,15 @@
coder v0.0.0-devel coder v0.0.0-devel
USAGE: USAGE:
coder groups delete <name> coder groups delete [flags] <name>
Delete a user group Delete a user group
Aliases: rm Aliases: rm
OPTIONS:
-O, --org string, $CODER_ORGANIZATION
Select which organization (uuid or name) to use.
——— ———
Run `coder --help` for a list of global options. Run `coder --help` for a list of global options.

View File

@ -6,6 +6,9 @@ USAGE:
Edit a user group Edit a user group
OPTIONS: OPTIONS:
-O, --org string, $CODER_ORGANIZATION
Select which organization (uuid or name) to use.
-a, --add-users string-array -a, --add-users string-array
Add users to the group. Accepts emails or IDs. Add users to the group. Accepts emails or IDs.

View File

@ -6,6 +6,9 @@ USAGE:
List user groups List user groups
OPTIONS: OPTIONS:
-O, --org string, $CODER_ORGANIZATION
Select which organization (uuid or name) to use.
-c, --column string-array (default: name,display name,organization id,members,avatar url) -c, --column string-array (default: name,display name,organization id,members,avatar url)
Columns to display in table output. Available columns: name, display Columns to display in table output. Available columns: name, display
name, organization id, members, avatar url. name, organization id, members, avatar url.