feat: add organization delete command to cli (#21940)

The API endpoints existed for this already, so this PR just adds CLI
functionality which uses those API endpoints.

closes #21891 

Generated with the help of Mux
This commit is contained in:
Rowan Smith
2026-02-05 19:35:20 +11:00
committed by GitHub
parent dc633e22a3
commit e3ce3c342a
8 changed files with 234 additions and 0 deletions
+1
View File
@@ -24,6 +24,7 @@ func (r *RootCmd) organizations() *serpent.Command {
Children: []*serpent.Command{ Children: []*serpent.Command{
r.showOrganization(orgContext), r.showOrganization(orgContext),
r.createOrganization(), r.createOrganization(),
r.deleteOrganization(orgContext),
r.organizationMembers(orgContext), r.organizationMembers(orgContext),
r.organizationRoles(orgContext), r.organizationRoles(orgContext),
r.organizationSettings(orgContext), r.organizationSettings(orgContext),
+122
View File
@@ -2,9 +2,11 @@ package cli_test
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"sync/atomic"
"testing" "testing"
"time" "time"
@@ -12,8 +14,10 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/pty/ptytest" "github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/pretty"
) )
func TestCurrentOrganization(t *testing.T) { func TestCurrentOrganization(t *testing.T) {
@@ -54,6 +58,124 @@ func TestCurrentOrganization(t *testing.T) {
}) })
} }
func TestOrganizationDelete(t *testing.T) {
t.Parallel()
t.Run("Yes", func(t *testing.T) {
t.Parallel()
orgID := uuid.New()
var deleteCalled atomic.Bool
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/v2/organizations/my-org":
_ = json.NewEncoder(w).Encode(codersdk.Organization{
MinimalOrganization: codersdk.MinimalOrganization{
ID: orgID,
Name: "my-org",
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
case r.Method == http.MethodDelete && r.URL.Path == fmt.Sprintf("/api/v2/organizations/%s", orgID.String()):
deleteCalled.Store(true)
w.WriteHeader(http.StatusOK)
default:
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
client := codersdk.New(must(url.Parse(server.URL)))
inv, root := clitest.New(t, "organizations", "delete", "my-org", "--yes")
clitest.SetupConfig(t, client, root)
require.NoError(t, inv.Run())
require.True(t, deleteCalled.Load(), "expected delete request")
})
t.Run("Prompted", func(t *testing.T) {
t.Parallel()
orgID := uuid.New()
var deleteCalled atomic.Bool
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/v2/organizations/my-org":
_ = json.NewEncoder(w).Encode(codersdk.Organization{
MinimalOrganization: codersdk.MinimalOrganization{
ID: orgID,
Name: "my-org",
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
case r.Method == http.MethodDelete && r.URL.Path == fmt.Sprintf("/api/v2/organizations/%s", orgID.String()):
deleteCalled.Store(true)
w.WriteHeader(http.StatusOK)
default:
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
client := codersdk.New(must(url.Parse(server.URL)))
inv, root := clitest.New(t, "organizations", "delete", "my-org")
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t).Attach(inv)
execDone := make(chan error)
go func() {
execDone <- inv.Run()
}()
pty.ExpectMatch(fmt.Sprintf("Delete organization %s?", pretty.Sprint(cliui.DefaultStyles.Code, "my-org")))
pty.WriteLine("yes")
require.NoError(t, <-execDone)
require.True(t, deleteCalled.Load(), "expected delete request")
})
t.Run("Default", func(t *testing.T) {
t.Parallel()
orgID := uuid.New()
var deleteCalled atomic.Bool
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/api/v2/organizations/default":
_ = json.NewEncoder(w).Encode(codersdk.Organization{
MinimalOrganization: codersdk.MinimalOrganization{
ID: orgID,
Name: "default",
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
IsDefault: true,
})
case r.Method == http.MethodDelete:
deleteCalled.Store(true)
w.WriteHeader(http.StatusOK)
default:
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
client := codersdk.New(must(url.Parse(server.URL)))
inv, root := clitest.New(t, "organizations", "delete", "default", "--yes")
clitest.SetupConfig(t, client, root)
err := inv.Run()
require.Error(t, err)
require.ErrorContains(t, err, "default organization")
require.False(t, deleteCalled.Load(), "expected no delete request")
})
}
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)
+65
View File
@@ -0,0 +1,65 @@
package cli
import (
"fmt"
"time"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/pretty"
"github.com/coder/serpent"
)
func (r *RootCmd) deleteOrganization(_ *OrganizationContext) *serpent.Command {
cmd := &serpent.Command{
Use: "delete <organization_name_or_id>",
Short: "Delete an organization",
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
),
Options: serpent.OptionSet{
cliui.SkipPromptOption(),
},
Handler: func(inv *serpent.Invocation) error {
client, err := r.InitClient(inv)
if err != nil {
return err
}
orgArg := inv.Args[0]
organization, err := client.OrganizationByName(inv.Context(), orgArg)
if err != nil {
return err
}
if organization.IsDefault {
return xerrors.Errorf("cannot delete the default organization %q", organization.Name)
}
_, err = cliui.Prompt(inv, cliui.PromptOptions{
Text: fmt.Sprintf("Delete organization %s?", pretty.Sprint(cliui.DefaultStyles.Code, organization.Name)),
IsConfirm: true,
Default: cliui.ConfirmNo,
})
if err != nil {
return err
}
err = client.DeleteOrganization(inv.Context(), organization.ID.String())
if err != nil {
return xerrors.Errorf("delete organization %q: %w", organization.Name, err)
}
_, _ = fmt.Fprintf(
inv.Stdout,
"Deleted organization %s at %s\n",
pretty.Sprint(cliui.DefaultStyles.Keyword, organization.Name),
cliui.Timestamp(time.Now()),
)
return nil
},
}
return cmd
}
+1
View File
@@ -9,6 +9,7 @@ USAGE:
SUBCOMMANDS: SUBCOMMANDS:
create Create a new organization. create Create a new organization.
delete Delete an organization
members Manage organization members members Manage organization members
roles Manage organization roles. roles Manage organization roles.
settings Manage organization settings. settings Manage organization settings.
+15
View File
@@ -0,0 +1,15 @@
coder v0.0.0-devel
USAGE:
coder organizations delete [flags] <organization_name_or_id>
Delete an organization
Aliases: rm
OPTIONS:
-y, --yes bool
Bypass confirmation prompts.
———
Run `coder --help` for a list of global options.
+5
View File
@@ -1627,6 +1627,11 @@
"description": "Create a new organization.", "description": "Create a new organization.",
"path": "reference/cli/organizations_create.md" "path": "reference/cli/organizations_create.md"
}, },
{
"title": "organizations delete",
"description": "Delete an organization",
"path": "reference/cli/organizations_delete.md"
},
{ {
"title": "organizations members", "title": "organizations members",
"description": "Manage organization members", "description": "Manage organization members",
+1
View File
@@ -21,6 +21,7 @@ coder organizations [flags] [subcommand]
|------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| |------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [<code>show</code>](./organizations_show.md) | 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. | | [<code>show</code>](./organizations_show.md) | 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. |
| [<code>create</code>](./organizations_create.md) | Create a new organization. | | [<code>create</code>](./organizations_create.md) | Create a new organization. |
| [<code>delete</code>](./organizations_delete.md) | Delete an organization |
| [<code>members</code>](./organizations_members.md) | Manage organization members | | [<code>members</code>](./organizations_members.md) | Manage organization members |
| [<code>roles</code>](./organizations_roles.md) | Manage organization roles. | | [<code>roles</code>](./organizations_roles.md) | Manage organization roles. |
| [<code>settings</code>](./organizations_settings.md) | Manage organization settings. | | [<code>settings</code>](./organizations_settings.md) | Manage organization settings. |
+24
View File
@@ -0,0 +1,24 @@
<!-- DO NOT EDIT | GENERATED CONTENT -->
# organizations delete
Delete an organization
Aliases:
* rm
## Usage
```console
coder organizations delete [flags] <organization_name_or_id>
```
## Options
### -y, --yes
| | |
|------|-------------------|
| Type | <code>bool</code> |
Bypass confirmation prompts.