mirror of https://github.com/coder/coder.git
feat: implement organization context in the cli (#12259)
* feat: implement organization context in the cli `coder org show current`
This commit is contained in:
parent
f44c89d200
commit
d2998c6b7b
|
@ -70,6 +70,14 @@ func (r Root) PostgresPort() File {
|
||||||
// File provides convenience methods for interacting with *os.File.
|
// File provides convenience methods for interacting with *os.File.
|
||||||
type File string
|
type File string
|
||||||
|
|
||||||
|
func (f File) Exists() bool {
|
||||||
|
if f == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err := os.Stat(string(f))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
// Delete deletes the file.
|
// Delete deletes the file.
|
||||||
func (f File) Delete() error {
|
func (f File) Delete() error {
|
||||||
if f == "" {
|
if f == "" {
|
||||||
|
|
|
@ -43,7 +43,7 @@ func (r *RootCmd) create() *clibase.Cmd {
|
||||||
),
|
),
|
||||||
Middleware: clibase.Chain(r.InitClient(client)),
|
Middleware: clibase.Chain(r.InitClient(client)),
|
||||||
Handler: func(inv *clibase.Invocation) error {
|
Handler: func(inv *clibase.Invocation) error {
|
||||||
organization, err := CurrentOrganization(inv, client)
|
organization, err := CurrentOrganization(r, inv, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/cli/clibase"
|
||||||
|
"github.com/coder/coder/v2/cli/cliui"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *RootCmd) organizations() *clibase.Cmd {
|
||||||
|
cmd := &clibase.Cmd{
|
||||||
|
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 *clibase.Invocation) error {
|
||||||
|
return inv.Command.HelpHandler(inv)
|
||||||
|
},
|
||||||
|
Children: []*clibase.Cmd{
|
||||||
|
r.currentOrganization(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Options = clibase.OptionSet{}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootCmd) currentOrganization() *clibase.Cmd {
|
||||||
|
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 "", fmt.Errorf("expected []Organization, got %T", data)
|
||||||
|
}
|
||||||
|
return stringFormat(typed)
|
||||||
|
}),
|
||||||
|
cliui.TableFormat([]codersdk.Organization{}, []string{"id", "name", "default"}),
|
||||||
|
cliui.JSONFormat(),
|
||||||
|
)
|
||||||
|
onlyID = false
|
||||||
|
)
|
||||||
|
cmd := &clibase.Cmd{
|
||||||
|
Use: "show [current|me|uuid]",
|
||||||
|
Short: "Show the organization, if no argument is given, the organization currently in use will be shown.",
|
||||||
|
Middleware: clibase.Chain(
|
||||||
|
r.InitClient(client),
|
||||||
|
clibase.RequireRangeArgs(0, 1),
|
||||||
|
),
|
||||||
|
Options: clibase.OptionSet{
|
||||||
|
{
|
||||||
|
Name: "only-id",
|
||||||
|
Description: "Only print the organization ID.",
|
||||||
|
Required: false,
|
||||||
|
Flag: "only-id",
|
||||||
|
Value: clibase.BoolOf(&onlyID),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Handler: func(inv *clibase.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 "", fmt.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 "", fmt.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
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package cli_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/coder/coder/v2/cli/clitest"
|
||||||
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||||
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
|
"github.com/coder/coder/v2/codersdk"
|
||||||
|
"github.com/coder/coder/v2/pty/ptytest"
|
||||||
|
"github.com/coder/coder/v2/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCurrentOrganization(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("OnlyID", 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
inv, root := clitest.New(t, "organizations", "show", "--only-id")
|
||||||
|
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(first.OrganizationID.String())
|
||||||
|
})
|
||||||
|
}
|
41
cli/root.go
41
cli/root.go
|
@ -94,6 +94,7 @@ func (r *RootCmd) Core() []*clibase.Cmd {
|
||||||
r.tokens(),
|
r.tokens(),
|
||||||
r.users(),
|
r.users(),
|
||||||
r.version(defaultVersionInfo),
|
r.version(defaultVersionInfo),
|
||||||
|
r.organizations(),
|
||||||
|
|
||||||
// Workspace Commands
|
// Workspace Commands
|
||||||
r.autoupdate(),
|
r.autoupdate(),
|
||||||
|
@ -698,14 +699,44 @@ func (r *RootCmd) createAgentClient() (*agentsdk.Client, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CurrentOrganization returns the currently active organization for the authenticated user.
|
// CurrentOrganization returns the currently active organization for the authenticated user.
|
||||||
func CurrentOrganization(inv *clibase.Invocation, client *codersdk.Client) (codersdk.Organization, error) {
|
func CurrentOrganization(r *RootCmd, inv *clibase.Invocation, client *codersdk.Client) (codersdk.Organization, error) {
|
||||||
|
conf := r.createConfig()
|
||||||
|
selected := ""
|
||||||
|
if conf.Organization().Exists() {
|
||||||
|
org, err := conf.Organization().Read()
|
||||||
|
if err != nil {
|
||||||
|
return codersdk.Organization{}, fmt.Errorf("read selected organization from config file %q: %w", conf.Organization(), err)
|
||||||
|
}
|
||||||
|
selected = org
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the org exists and the user is a member
|
||||||
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{}, nil
|
return codersdk.Organization{}, err
|
||||||
}
|
}
|
||||||
// For now, we won't use the config to set this.
|
|
||||||
// Eventually, we will support changing using "coder switch <org>"
|
// User manually selected an organization
|
||||||
return orgs[0], nil
|
if selected != "" {
|
||||||
|
index := slices.IndexFunc(orgs, func(org codersdk.Organization) bool {
|
||||||
|
return org.Name == selected || org.ID.String() == selected
|
||||||
|
})
|
||||||
|
|
||||||
|
if index < 0 {
|
||||||
|
return codersdk.Organization{}, xerrors.Errorf("organization %q not found, are you sure you are a member of this organization?", selected)
|
||||||
|
}
|
||||||
|
return orgs[index], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// User did not select an organization, so use the default.
|
||||||
|
index := slices.IndexFunc(orgs, func(org codersdk.Organization) bool {
|
||||||
|
return org.IsDefault
|
||||||
|
})
|
||||||
|
if index < 0 {
|
||||||
|
return codersdk.Organization{}, xerrors.Errorf("unable to determine current organization. Use 'coder organizations switch <org>' to select an organization to use")
|
||||||
|
}
|
||||||
|
|
||||||
|
return orgs[index], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func splitNamedWorkspace(identifier string) (owner string, workspaceName string, err error) {
|
func splitNamedWorkspace(identifier string) (owner string, workspaceName string, err error) {
|
||||||
|
|
|
@ -69,7 +69,7 @@ func (r *RootCmd) templateCreate() *clibase.Cmd {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
organization, err := CurrentOrganization(inv, client)
|
organization, err := CurrentOrganization(r, inv, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -243,19 +243,7 @@ func TestTemplateCreate(t *testing.T) {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
matches := []struct {
|
pty.ExpectMatch("context canceled")
|
||||||
match string
|
|
||||||
write string
|
|
||||||
}{
|
|
||||||
{match: "Upload", write: "yes"},
|
|
||||||
}
|
|
||||||
for _, m := range matches {
|
|
||||||
pty.ExpectMatch(m.match)
|
|
||||||
if len(m.write) > 0 {
|
|
||||||
pty.WriteLine(m.write)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ func (r *RootCmd) templateDelete() *clibase.Cmd {
|
||||||
templates = []codersdk.Template{}
|
templates = []codersdk.Template{}
|
||||||
)
|
)
|
||||||
|
|
||||||
organization, err := CurrentOrganization(inv, client)
|
organization, err := CurrentOrganization(r, inv, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,7 +79,7 @@ func (r *RootCmd) templateEdit() *clibase.Cmd {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
organization, err := CurrentOrganization(inv, client)
|
organization, err := CurrentOrganization(r, inv, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("get current organization: %w", err)
|
return xerrors.Errorf("get current organization: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ func (r *RootCmd) templateList() *clibase.Cmd {
|
||||||
r.InitClient(client),
|
r.InitClient(client),
|
||||||
),
|
),
|
||||||
Handler: func(inv *clibase.Invocation) error {
|
Handler: func(inv *clibase.Invocation) error {
|
||||||
organization, err := CurrentOrganization(inv, client)
|
organization, err := CurrentOrganization(r, inv, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ func (r *RootCmd) templatePull() *clibase.Cmd {
|
||||||
return xerrors.Errorf("either tar or zip can be selected")
|
return xerrors.Errorf("either tar or zip can be selected")
|
||||||
}
|
}
|
||||||
|
|
||||||
organization, err := CurrentOrganization(inv, client)
|
organization, err := CurrentOrganization(r, inv, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("get current organization: %w", err)
|
return xerrors.Errorf("get current organization: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ func (r *RootCmd) templatePush() *clibase.Cmd {
|
||||||
Handler: func(inv *clibase.Invocation) error {
|
Handler: func(inv *clibase.Invocation) error {
|
||||||
uploadFlags.setWorkdir(workdir)
|
uploadFlags.setWorkdir(workdir)
|
||||||
|
|
||||||
organization, err := CurrentOrganization(inv, client)
|
organization, err := CurrentOrganization(r, inv, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ func (r *RootCmd) setArchiveTemplateVersion(archive bool) *clibase.Cmd {
|
||||||
versions []codersdk.TemplateVersion
|
versions []codersdk.TemplateVersion
|
||||||
)
|
)
|
||||||
|
|
||||||
organization, err := CurrentOrganization(inv, client)
|
organization, err := CurrentOrganization(r, inv, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,7 @@ func (r *RootCmd) archiveTemplateVersions() *clibase.Cmd {
|
||||||
templates = []codersdk.Template{}
|
templates = []codersdk.Template{}
|
||||||
)
|
)
|
||||||
|
|
||||||
organization, err := CurrentOrganization(inv, client)
|
organization, err := CurrentOrganization(r, inv, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,7 @@ func (r *RootCmd) templateVersionsList() *clibase.Cmd {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Handler: func(inv *clibase.Invocation) error {
|
Handler: func(inv *clibase.Invocation) error {
|
||||||
organization, err := CurrentOrganization(inv, client)
|
organization, err := CurrentOrganization(r, inv, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("get current organization: %w", err)
|
return xerrors.Errorf("get current organization: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ func (r *RootCmd) userCreate() *clibase.Cmd {
|
||||||
r.InitClient(client),
|
r.InitClient(client),
|
||||||
),
|
),
|
||||||
Handler: func(inv *clibase.Invocation) error {
|
Handler: func(inv *clibase.Invocation) error {
|
||||||
organization, err := CurrentOrganization(inv, client)
|
organization, err := CurrentOrganization(r, inv, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,12 @@ package httpmw
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/database"
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||||
"github.com/coder/coder/v2/coderd/httpapi"
|
"github.com/coder/coder/v2/coderd/httpapi"
|
||||||
|
@ -40,19 +44,34 @@ func ExtractOrganizationParam(db database.Store) func(http.Handler) http.Handler
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
orgID, ok := ParseUUIDParam(rw, r, "organization")
|
arg := chi.URLParam(r, "organization")
|
||||||
if !ok {
|
if arg == "" {
|
||||||
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
|
Message: "\"organization\" must be provided.",
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
organization, err := db.GetOrganizationByID(ctx, orgID)
|
var organization database.Organization
|
||||||
|
var err error
|
||||||
|
// Try by name or uuid.
|
||||||
|
id, err := uuid.Parse(arg)
|
||||||
|
if err == nil {
|
||||||
|
organization, err = db.GetOrganizationByID(ctx, id)
|
||||||
|
} else {
|
||||||
|
organization, err = db.GetOrganizationByName(ctx, arg)
|
||||||
|
}
|
||||||
if httpapi.Is404Error(err) {
|
if httpapi.Is404Error(err) {
|
||||||
httpapi.ResourceNotFound(rw)
|
httpapi.ResourceNotFound(rw)
|
||||||
|
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
||||||
|
Message: fmt.Sprintf("Organization %q not found.", arg),
|
||||||
|
Detail: "Provide either the organization id or name.",
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||||
Message: "Internal error fetching organization.",
|
Message: fmt.Sprintf("Internal error fetching organization %q.", arg),
|
||||||
Detail: err.Error(),
|
Detail: err.Error(),
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
|
@ -103,7 +103,7 @@ func TestOrganizationParam(t *testing.T) {
|
||||||
rtr.ServeHTTP(rw, r)
|
rtr.ServeHTTP(rw, r)
|
||||||
res := rw.Result()
|
res := rw.Result()
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
require.Equal(t, http.StatusBadRequest, res.StatusCode)
|
require.Equal(t, http.StatusNotFound, res.StatusCode)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("NotInOrganization", func(t *testing.T) {
|
t.Run("NotInOrganization", func(t *testing.T) {
|
||||||
|
@ -160,8 +160,6 @@ func TestOrganizationParam(t *testing.T) {
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
chi.RouteContext(r.Context()).URLParams.Add("organization", organization.ID.String())
|
|
||||||
chi.RouteContext(r.Context()).URLParams.Add("user", user.ID.String())
|
|
||||||
rtr.Use(
|
rtr.Use(
|
||||||
httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
|
httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
|
||||||
DB: db,
|
DB: db,
|
||||||
|
@ -194,9 +192,21 @@ func TestOrganizationParam(t *testing.T) {
|
||||||
assert.NotEmpty(t, orgMem.OrganizationMember.UserID)
|
assert.NotEmpty(t, orgMem.OrganizationMember.UserID)
|
||||||
assert.NotEmpty(t, orgMem.OrganizationMember.Roles)
|
assert.NotEmpty(t, orgMem.OrganizationMember.Roles)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Try by ID
|
||||||
|
chi.RouteContext(r.Context()).URLParams.Add("organization", organization.ID.String())
|
||||||
|
chi.RouteContext(r.Context()).URLParams.Add("user", user.ID.String())
|
||||||
rtr.ServeHTTP(rw, r)
|
rtr.ServeHTTP(rw, r)
|
||||||
res := rw.Result()
|
res := rw.Result()
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
require.Equal(t, http.StatusOK, res.StatusCode, "by id")
|
||||||
|
|
||||||
|
// Try by name
|
||||||
|
chi.RouteContext(r.Context()).URLParams.Add("organization", organization.Name)
|
||||||
|
chi.RouteContext(r.Context()).URLParams.Add("user", user.ID.String())
|
||||||
|
rtr.ServeHTTP(rw, r)
|
||||||
|
res = rw.Result()
|
||||||
|
defer res.Body.Close()
|
||||||
|
require.Equal(t, http.StatusOK, res.StatusCode, "by name")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,7 @@ func TestOrganizationByUserAndName(t *testing.T) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
_, err := client.OrganizationByName(ctx, codersdk.Me, "nothing")
|
_, err := client.OrganizationByUserAndName(ctx, codersdk.Me, "nothing")
|
||||||
var apiErr *codersdk.Error
|
var apiErr *codersdk.Error
|
||||||
require.ErrorAs(t, err, &apiErr)
|
require.ErrorAs(t, err, &apiErr)
|
||||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||||
|
@ -85,7 +85,7 @@ func TestOrganizationByUserAndName(t *testing.T) {
|
||||||
Name: "another",
|
Name: "another",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = other.OrganizationByName(ctx, codersdk.Me, org.Name)
|
_, err = other.OrganizationByUserAndName(ctx, codersdk.Me, org.Name)
|
||||||
var apiErr *codersdk.Error
|
var apiErr *codersdk.Error
|
||||||
require.ErrorAs(t, err, &apiErr)
|
require.ErrorAs(t, err, &apiErr)
|
||||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||||
|
@ -101,7 +101,7 @@ func TestOrganizationByUserAndName(t *testing.T) {
|
||||||
|
|
||||||
org, err := client.Organization(ctx, user.OrganizationID)
|
org, err := client.Organization(ctx, user.OrganizationID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
_, err = client.OrganizationByName(ctx, codersdk.Me, org.Name)
|
_, err = client.OrganizationByUserAndName(ctx, codersdk.Me, org.Name)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,11 +26,11 @@ const (
|
||||||
|
|
||||||
// Organization is the JSON representation of a Coder organization.
|
// Organization is the JSON representation of a Coder organization.
|
||||||
type Organization struct {
|
type Organization struct {
|
||||||
ID uuid.UUID `json:"id" validate:"required" format:"uuid"`
|
ID uuid.UUID `table:"id" json:"id" validate:"required" format:"uuid"`
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `table:"name,default_sort" json:"name" validate:"required"`
|
||||||
CreatedAt time.Time `json:"created_at" validate:"required" format:"date-time"`
|
CreatedAt time.Time `table:"created_at" json:"created_at" validate:"required" format:"date-time"`
|
||||||
UpdatedAt time.Time `json:"updated_at" validate:"required" format:"date-time"`
|
UpdatedAt time.Time `table:"updated_at" json:"updated_at" validate:"required" format:"date-time"`
|
||||||
IsDefault bool `json:"is_default" validate:"required"`
|
IsDefault bool `table:"default" json:"is_default" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrganizationMember struct {
|
type OrganizationMember struct {
|
||||||
|
@ -153,8 +153,8 @@ type CreateWorkspaceRequest struct {
|
||||||
AutomaticUpdates AutomaticUpdates `json:"automatic_updates,omitempty"`
|
AutomaticUpdates AutomaticUpdates `json:"automatic_updates,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization, error) {
|
func (c *Client) OrganizationByName(ctx context.Context, name string) (Organization, error) {
|
||||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s", id.String()), nil)
|
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s", name), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Organization{}, xerrors.Errorf("execute request: %w", err)
|
return Organization{}, xerrors.Errorf("execute request: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -168,6 +168,12 @@ func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization,
|
||||||
return organization, json.NewDecoder(res.Body).Decode(&organization)
|
return organization, json.NewDecoder(res.Body).Decode(&organization)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization, error) {
|
||||||
|
// OrganizationByName uses the exact same endpoint. It accepts a name or uuid.
|
||||||
|
// We just provide this function for type safety.
|
||||||
|
return c.OrganizationByName(ctx, id.String())
|
||||||
|
}
|
||||||
|
|
||||||
// ProvisionerDaemons returns provisioner daemons available.
|
// ProvisionerDaemons returns provisioner daemons available.
|
||||||
func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) {
|
func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) {
|
||||||
res, err := c.Request(ctx, http.MethodGet,
|
res, err := c.Request(ctx, http.MethodGet,
|
||||||
|
|
|
@ -573,7 +573,7 @@ func (c *Client) OrganizationsByUser(ctx context.Context, user string) ([]Organi
|
||||||
return orgs, json.NewDecoder(res.Body).Decode(&orgs)
|
return orgs, json.NewDecoder(res.Body).Decode(&orgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) OrganizationByName(ctx context.Context, user string, name string) (Organization, error) {
|
func (c *Client) OrganizationByUserAndName(ctx context.Context, user string, name string) (Organization, error) {
|
||||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations/%s", user, name), nil)
|
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations/%s", user, name), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Organization{}, err
|
return Organization{}, err
|
||||||
|
|
|
@ -29,7 +29,7 @@ func (r *RootCmd) groupCreate() *clibase.Cmd {
|
||||||
Handler: func(inv *clibase.Invocation) error {
|
Handler: func(inv *clibase.Invocation) error {
|
||||||
ctx := inv.Context()
|
ctx := inv.Context()
|
||||||
|
|
||||||
org, err := agpl.CurrentOrganization(inv, client)
|
org, err := agpl.CurrentOrganization(&r.RootCmd, inv, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("current organization: %w", err)
|
return xerrors.Errorf("current organization: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ func (r *RootCmd) groupDelete() *clibase.Cmd {
|
||||||
groupName = inv.Args[0]
|
groupName = inv.Args[0]
|
||||||
)
|
)
|
||||||
|
|
||||||
org, err := agpl.CurrentOrganization(inv, client)
|
org, err := agpl.CurrentOrganization(&r.RootCmd, inv, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("current organization: %w", err)
|
return xerrors.Errorf("current organization: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ func (r *RootCmd) groupEdit() *clibase.Cmd {
|
||||||
groupName = inv.Args[0]
|
groupName = inv.Args[0]
|
||||||
)
|
)
|
||||||
|
|
||||||
org, err := agpl.CurrentOrganization(inv, client)
|
org, err := agpl.CurrentOrganization(&r.RootCmd, inv, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("current organization: %w", err)
|
return xerrors.Errorf("current organization: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ func (r *RootCmd) groupList() *clibase.Cmd {
|
||||||
Handler: func(inv *clibase.Invocation) error {
|
Handler: func(inv *clibase.Invocation) error {
|
||||||
ctx := inv.Context()
|
ctx := inv.Context()
|
||||||
|
|
||||||
org, err := agpl.CurrentOrganization(inv, client)
|
org, err := agpl.CurrentOrganization(&r.RootCmd, inv, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("current organization: %w", err)
|
return xerrors.Errorf("current organization: %w", err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue