Skip to content

feat: implement organization context in the cli #12259

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Feb 26, 2024
Prev Previous commit
Next Next commit
implement more general organizations show
  • Loading branch information
Emyrk committed Feb 23, 2024
commit f8b97d981d9f2392094ae548f1df313f052189c3
71 changes: 58 additions & 13 deletions cli/organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"fmt"
"strings"

"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
Expand Down Expand Up @@ -29,29 +30,28 @@ func (r *RootCmd) organizations() *clibase.Cmd {

func (r *RootCmd) currentOrganization() *clibase.Cmd {
var (
client = new(codersdk.Client)
formatter = cliui.NewOutputFormatter(
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)
}
if len(typed) != 1 {
return "", fmt.Errorf("expected 1 organization, got %d", len(typed))
}
return fmt.Sprintf("Current organization: %s (%s)\n", typed[0].Name, typed[0].ID.String()), nil
return stringFormat(typed)
}),
cliui.TableFormat([]codersdk.Organization{}, []string{"id", "name", "default"}),
cliui.JSONFormat(),
)
onlyID = false
)
cmd := &clibase.Cmd{
Use: "current",
Short: "Show the current selected organization the cli will use.",
Use: "show [current|me|uuid]",
Short: "Show organization information. By default, if no argument is provided, the current cli configured organization is shown.",
Middleware: clibase.Chain(
r.InitClient(client),
clibase.RequireRangeArgs(0, 1),
),
Options: clibase.OptionSet{
{
Expand All @@ -63,15 +63,60 @@ func (r *RootCmd) currentOrganization() *clibase.Cmd {
},
},
Handler: func(inv *clibase.Invocation) error {
org, err := CurrentOrganization(r, inv, client)
if err != nil {
return err
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 {
_, _ = fmt.Fprintf(inv.Stdout, "%s\n", org.ID)
for _, org := range orgs {
_, _ = fmt.Fprintf(inv.Stdout, "%s\n", org.ID)
}
} else {
out, err := formatter.Format(inv.Context(), []codersdk.Organization{org})
out, err := formatter.Format(inv.Context(), orgs)
if err != nil {
return err
}
Expand Down
27 changes: 23 additions & 4 deletions coderd/httpmw/organizationparam.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package httpmw

import (
"context"
"fmt"
"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/dbauthz"
"github.com/coder/coder/v2/coderd/httpapi"
Expand Down Expand Up @@ -40,19 +44,34 @@ func ExtractOrganizationParam(db database.Store) func(http.Handler) http.Handler
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
orgID, ok := ParseUUIDParam(rw, r, "organization")
if !ok {
arg := chi.URLParam(r, "organization")
if arg == "" {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "\"organization\" must be provided.",
})
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) {
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
}
if err != nil {
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(),
})
return
Expand Down
18 changes: 14 additions & 4 deletions coderd/httpmw/organizationparam_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func TestOrganizationParam(t *testing.T) {
rtr.ServeHTTP(rw, r)
res := rw.Result()
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) {
Expand Down Expand Up @@ -160,8 +160,6 @@ func TestOrganizationParam(t *testing.T) {
})
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(
httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
DB: db,
Expand Down Expand Up @@ -194,9 +192,21 @@ func TestOrganizationParam(t *testing.T) {
assert.NotEmpty(t, orgMem.OrganizationMember.UserID)
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)
res := rw.Result()
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")
})
}
10 changes: 8 additions & 2 deletions codersdk/organizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ type CreateWorkspaceRequest struct {
AutomaticUpdates AutomaticUpdates `json:"automatic_updates,omitempty"`
}

func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s", id.String()), nil)
func (c *Client) OrganizationByName(ctx context.Context, name string) (Organization, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s", name), nil)
if err != nil {
return Organization{}, xerrors.Errorf("execute request: %w", err)
}
Expand All @@ -168,6 +168,12 @@ func (c *Client) Organization(ctx context.Context, id uuid.UUID) (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.
func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) {
res, err := c.Request(ctx, http.MethodGet,
Expand Down
2 changes: 1 addition & 1 deletion codersdk/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ func (c *Client) OrganizationsByUser(ctx context.Context, user string) ([]Organi
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)
if err != nil {
return Organization{}, err
Expand Down