Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ The CLI currently focuses on authentication, cluster inspection, and applying re
Calls the Kedify API and transparently reads all pages before printing the final cluster list.
- `kedify get cluster [name-or-id]`
Prints one cluster by name or id, and shows an interactive picker when no name is provided.
- `kedify delete cluster [name-or-id]`
Deletes one cluster by name or id, and shows an interactive picker when no name is provided.
- `kedify list recommendations <cluster-id>`
Prints the recommendations payload for a cluster id.
- `kedify apply recommendations <kind/name>`
Applies recommendations from a saved JSON or YAML file to a Helm values file and can emit `json`, `diff`, or `override` output.
- Output formatting
`kedify list clusters`, `kedify get cluster`, and `kedify list recommendations` support `-o` and `--output` with `text`, `json`, or `yaml`. `text` is the default.
`kedify list clusters`, `kedify get cluster`, and `kedify list recommendations` support `-o` and `--output` with `text`, `json`, or `yaml`. `text` is the default. `kedify delete cluster` prints its confirmation message to `stderr` and keeps `stdout` empty for shell-friendly usage.

## Build

Expand Down Expand Up @@ -119,6 +121,18 @@ Get a cluster as JSON:
./bin/kedify get cluster my-cluster -o json
```

Delete a cluster by name:

```bash
./bin/kedify delete cluster my-cluster
```

Delete a cluster by UUID:

```bash
./bin/kedify delete cluster fc6af0dc-685b-4055-805d-0d3e0ead1596
```

List recommendations for a cluster as JSON:

```bash
Expand Down Expand Up @@ -166,6 +180,12 @@ Pick a cluster interactively:
./bin/kedify get cluster
```

Pick a cluster interactively and delete it:

```bash
./bin/kedify delete cluster
```

Override the API URL:

```bash
Expand Down
39 changes: 28 additions & 11 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ func (c *Client) GetRecommendations(apiURL, token, clusterID string) (any, error
return nil, fmt.Errorf("request recommendations for cluster %s: %w", clusterID, err)
}

func (c *Client) DeleteCluster(apiURL, token, clusterID string) error {
if _, err := c.doRequest(apiURL, token, http.MethodDelete, "/clusters/"+url.PathEscape(clusterID), nil); err != nil {
return fmt.Errorf("delete cluster %s: %w", clusterID, err)
}

return nil
}

func (c *Client) listPaginatedItems(apiURL, token, path string) ([]any, error) {
var allItems []any
page := 1
Expand Down Expand Up @@ -188,33 +196,42 @@ func readResponseBody(resp *http.Response) ([]byte, error) {
}

func (c *Client) getJSON(apiURL, token, path string, target any) error {
body, err := c.doRequest(apiURL, token, http.MethodGet, path, nil)
if err != nil {
return err
}

if err := json.Unmarshal(body, target); err != nil {
return fmt.Errorf("parse response as json: %w", err)
}

return nil
}

func (c *Client) doRequest(apiURL, token, method, path string, body io.Reader) ([]byte, error) {
requestURL := strings.TrimRight(apiURL, "/") + path

req, err := http.NewRequest(http.MethodGet, requestURL, nil)
req, err := http.NewRequest(method, requestURL, body)
if err != nil {
return fmt.Errorf("build request: %w", err)
return nil, fmt.Errorf("build request: %w", err)
}

req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json")

resp, err := c.httpClient.Do(req)
if err != nil {
return err
return nil, err
}

body, err := readResponseBody(resp)
responseBody, err := readResponseBody(resp)
if err != nil {
return err
return nil, err
}

if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("request failed with status %s: %s", resp.Status, strings.TrimSpace(string(body)))
}

if err := json.Unmarshal(body, target); err != nil {
return fmt.Errorf("parse response as json: %w", err)
return nil, fmt.Errorf("request failed with status %s: %s", resp.Status, strings.TrimSpace(string(responseBody)))
}

return nil
return responseBody, nil
}
22 changes: 22 additions & 0 deletions internal/api/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,25 @@ func TestGetRecommendationsFallsBackToNonPaginatedPayload(t *testing.T) {
t.Fatalf("requests = %d, want 1", requests)
}
}

func TestDeleteClusterCallsDedicatedEndpoint(t *testing.T) {
client := &Client{httpClient: &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
if r.Method != http.MethodDelete {
t.Fatalf("method = %q", r.Method)
}
if r.URL.Path != "/v1/clusters/fc6af0dc-685b-4055-805d-0d3e0ead1596" {
t.Fatalf("path = %q", r.URL.Path)
}

return &http.Response{
StatusCode: http.StatusNoContent,
Status: "204 No Content",
Body: io.NopCloser(strings.NewReader("")),
Header: make(http.Header),
}, nil
})}}

if err := client.DeleteCluster("https://api.dev.kedify.io/v1", "token", "fc6af0dc-685b-4055-805d-0d3e0ead1596"); err != nil {
t.Fatalf("DeleteCluster() error = %v", err)
}
}
80 changes: 80 additions & 0 deletions internal/cli/delete/cluster.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package delete

import (
"fmt"

clictx "github.com/kedify/cli/internal/cli/context"
getcmd "github.com/kedify/cli/internal/cli/get"
)

type DeleteClusterCmd struct {
Name string `arg:"" optional:"" name:"name" help:"Cluster name or id."`
}

func (c *DeleteClusterCmd) Run(ctx *clictx.Context) error {
token, err := clictx.ResolveToken(ctx)
if err != nil {
return err
}

clusterID, clusterName, err := c.resolveCluster(ctx, token)
if err != nil {
return err
}

if err := ctx.Client.DeleteCluster(ctx.APIURL, token, clusterID); err != nil {
return err
}

if clusterName != "" {
_, err = fmt.Fprintf(ctx.Stderr, "Deleted cluster %q (%s).\n", clusterName, clusterID)
} else {
_, err = fmt.Fprintf(ctx.Stderr, "Deleted cluster %s.\n", clusterID)
}

return err
}

func (c *DeleteClusterCmd) resolveCluster(ctx *clictx.Context, token string) (string, string, error) {
if c.Name != "" && getcmd.IsUUID(c.Name) {
cluster, err := ctx.Client.GetCluster(ctx.APIURL, token, c.Name)
if err == nil {
return c.Name, clusterString(cluster, "name"), nil
}
return c.Name, "", nil
}

clusters, err := ctx.Client.ListClusters(ctx.APIURL, token)
if err != nil {
return "", "", err
}

if c.Name != "" {
cluster, err := getcmd.FindCluster(clusters, c.Name)
if err != nil {
return "", "", err
}
return clusterString(cluster, "id"), clusterString(cluster, "name"), nil
}

cluster, err := ctx.SelectCluster(ctx.Stdin, ctx.Stdout, ctx.Stderr, clusters)
if err != nil {
return "", "", err
}

return clusterString(cluster, "id"), clusterString(cluster, "name"), nil
}

func clusterString(cluster map[string]any, key string) string {
value, ok := cluster[key]
if !ok {
return ""
}

text, ok := value.(string)
if !ok {
return ""
}

return text
}
Loading
Loading