mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
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:
@@ -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),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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",
|
||||||
|
|||||||
Generated
+1
@@ -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
@@ -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.
|
||||||
Reference in New Issue
Block a user