Skip to content
Open
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
1 change: 1 addition & 0 deletions internal/cmd/iam/iam.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ func NewCommand(s *state.State) *cobra.Command {
Long: `Manage IAM resources for the Qdrant Cloud account.`,
Args: cobra.NoArgs,
}
cmd.AddCommand(newKeyCommand(s))
return cmd
}
25 changes: 25 additions & 0 deletions internal/cmd/iam/key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package iam

import (
"github.com/spf13/cobra"

"github.com/qdrant/qcloud-cli/internal/state"
)

func newKeyCommand(s *state.State) *cobra.Command {
cmd := &cobra.Command{
Use: "key",
Short: "Manage cloud management keys",
Long: `Manage cloud management keys for the account.

Management keys authenticate requests to the Qdrant Cloud API. Use them to authorize
the CLI, automation scripts, or any other tooling that calls the Qdrant Cloud API.`,
Args: cobra.NoArgs,
}
cmd.AddCommand(
newKeyListCommand(s),
newKeyCreateCommand(s),
newKeyDeleteCommand(s),
)
return cmd
}
66 changes: 66 additions & 0 deletions internal/cmd/iam/key_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package iam

import (
"fmt"
"io"

"github.com/spf13/cobra"

authv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/auth/v1"

"github.com/qdrant/qcloud-cli/internal/cmd/base"
"github.com/qdrant/qcloud-cli/internal/state"
)

func newKeyCreateCommand(s *state.State) *cobra.Command {
return base.CreateCmd[*authv1.ManagementKey]{
Long: `Create a new cloud management key for the account.

Management keys grant access to the Qdrant Cloud API. The full key value is returned
only once at creation time — store it securely, as it cannot be retrieved again. If a
key is lost, delete it and create a new one.`,
Example: `# Create a new management key
qcloud iam key create

# Create and capture the key value in a script
qcloud iam key create --json | jq -r '.key'`,
BaseCobraCommand: func() *cobra.Command {
return &cobra.Command{
Use: "create",
Short: "Create a cloud management key",
Args: cobra.NoArgs,
}
},
Run: func(s *state.State, cmd *cobra.Command, args []string) (*authv1.ManagementKey, error) {
ctx := cmd.Context()
client, err := s.Client(ctx)
if err != nil {
return nil, err
}

accountID, err := s.AccountID()
if err != nil {
return nil, err
}

resp, err := client.Auth().CreateManagementKey(ctx, &authv1.CreateManagementKeyRequest{
ManagementKey: &authv1.ManagementKey{
AccountId: accountID,
},
})
if err != nil {
return nil, fmt.Errorf("failed to create management key: %w", err)
}

return resp.GetManagementKey(), nil
},
PrintResource: func(_ *cobra.Command, out io.Writer, key *authv1.ManagementKey) {
fmt.Fprintf(out, "Management key %s created.\n", key.GetId())
if k := key.GetKey(); k != "" {
fmt.Fprintln(out, "")
fmt.Fprintln(out, "Save this key now — it will not be shown again:")
fmt.Fprintf(out, " %s\n", k)
}
},
}.CobraCommand(s)
}
66 changes: 66 additions & 0 deletions internal/cmd/iam/key_create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package iam_test

import (
"encoding/json"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

authv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/auth/v1"

"github.com/qdrant/qcloud-cli/internal/testutil"
)

func TestKeyCreate_PrintsIDAndKey(t *testing.T) {
env := testutil.NewTestEnv(t, testutil.WithAccountID("test-account-id"))

env.AuthServer.CreateManagementKeyCalls.Returns(&authv1.CreateManagementKeyResponse{
ManagementKey: &authv1.ManagementKey{
Id: "new-key-id",
Key: "super-secret-value",
},
}, nil)

stdout, _, err := testutil.Exec(t, env, "iam", "key", "create")
require.NoError(t, err)
assert.Contains(t, stdout, "new-key-id")
assert.Contains(t, stdout, "super-secret-value")
assert.Contains(t, stdout, "Save this key now")

req, ok := env.AuthServer.CreateManagementKeyCalls.Last()
require.True(t, ok)
assert.Equal(t, "test-account-id", req.GetManagementKey().GetAccountId())
}

func TestKeyCreate_BackendError(t *testing.T) {
env := testutil.NewTestEnv(t)

env.AuthServer.CreateManagementKeyCalls.Returns(nil, fmt.Errorf("internal server error"))

_, _, err := testutil.Exec(t, env, "iam", "key", "create")
require.Error(t, err)
}

func TestKeyCreate_JSONOutput(t *testing.T) {
env := testutil.NewTestEnv(t)

env.AuthServer.CreateManagementKeyCalls.Returns(&authv1.CreateManagementKeyResponse{
ManagementKey: &authv1.ManagementKey{
Id: "json-key-id",
Key: "secret",
},
}, nil)

stdout, _, err := testutil.Exec(t, env, "iam", "key", "create", "--json")
require.NoError(t, err)

var result struct {
ID string `json:"id"`
Key string `json:"key"`
}
require.NoError(t, json.Unmarshal([]byte(stdout), &result))
assert.Equal(t, "json-key-id", result.ID)
assert.Equal(t, "secret", result.Key)
}
71 changes: 71 additions & 0 deletions internal/cmd/iam/key_delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package iam

import (
"fmt"

"github.com/spf13/cobra"

authv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/auth/v1"

"github.com/qdrant/qcloud-cli/internal/cmd/base"
"github.com/qdrant/qcloud-cli/internal/cmd/completion"
"github.com/qdrant/qcloud-cli/internal/cmd/util"
"github.com/qdrant/qcloud-cli/internal/state"
)

func newKeyDeleteCommand(s *state.State) *cobra.Command {
return base.Cmd{
Long: `Delete a cloud management key from the account.

Deleting a key immediately revokes its access to the Qdrant Cloud API. Any client
using the deleted key will receive authentication errors. This action cannot be undone.

A confirmation prompt is shown unless --force is passed.`,
Example: `# Delete a management key (with confirmation prompt)
qcloud iam key delete a1b2c3d4-e5f6-7890-abcd-ef1234567890

# Delete without confirmation
qcloud iam key delete a1b2c3d4-e5f6-7890-abcd-ef1234567890 --force`,
BaseCobraCommand: func() *cobra.Command {
cmd := &cobra.Command{
Use: "delete <key-id>",
Short: "Delete a cloud management key",
Args: util.ExactArgs(1, "a management key ID"),
}
cmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
return cmd
},
ValidArgsFunction: completion.ManagementKeyIDCompletion(s),
Run: func(s *state.State, cmd *cobra.Command, args []string) error {
keyID := args[0]

force, _ := cmd.Flags().GetBool("force")
if !util.ConfirmAction(force, cmd.ErrOrStderr(), fmt.Sprintf("Are you sure you want to delete management key %s?", keyID)) {
fmt.Fprintln(cmd.OutOrStdout(), "Aborted.")
return nil
}

ctx := cmd.Context()
client, err := s.Client(ctx)
if err != nil {
return err
}

accountID, err := s.AccountID()
if err != nil {
return err
}

_, err = client.Auth().DeleteManagementKey(ctx, &authv1.DeleteManagementKeyRequest{
AccountId: accountID,
ManagementKeyId: keyID,
})
if err != nil {
return fmt.Errorf("failed to delete management key: %w", err)
}

fmt.Fprintf(cmd.OutOrStdout(), "Management key %s deleted.\n", keyID)
return nil
},
}.CobraCommand(s)
}
72 changes: 72 additions & 0 deletions internal/cmd/iam/key_delete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package iam_test

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

authv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/auth/v1"

"github.com/qdrant/qcloud-cli/internal/testutil"
)

func TestKeyDelete_WithForce(t *testing.T) {
env := testutil.NewTestEnv(t, testutil.WithAccountID("test-account-id"))

env.AuthServer.DeleteManagementKeyCalls.Returns(&authv1.DeleteManagementKeyResponse{}, nil)

stdout, _, err := testutil.Exec(t, env, "iam", "key", "delete", "key-abc", "--force")
require.NoError(t, err)
assert.Contains(t, stdout, "key-abc")
assert.Contains(t, stdout, "deleted")

req, ok := env.AuthServer.DeleteManagementKeyCalls.Last()
require.True(t, ok)
assert.Equal(t, "test-account-id", req.GetAccountId())
assert.Equal(t, "key-abc", req.GetManagementKeyId())
}

func TestKeyDelete_Aborted(t *testing.T) {
env := testutil.NewTestEnv(t)

stdout, _, err := testutil.Exec(t, env, "iam", "key", "delete", "key-abc")
require.NoError(t, err)
assert.Contains(t, stdout, "Aborted.")
assert.Equal(t, 0, env.AuthServer.DeleteManagementKeyCalls.Count())
}

func TestKeyDelete_BackendError(t *testing.T) {
env := testutil.NewTestEnv(t)

env.AuthServer.DeleteManagementKeyCalls.Returns(nil, fmt.Errorf("internal server error"))

_, _, err := testutil.Exec(t, env, "iam", "key", "delete", "key-abc", "--force")
require.Error(t, err)
}

func TestKeyDelete_MissingArg(t *testing.T) {
env := testutil.NewTestEnv(t)

_, _, err := testutil.Exec(t, env, "iam", "key", "delete")
require.Error(t, err)
}

func TestKeyDeleteCompletion(t *testing.T) {
env := testutil.NewTestEnv(t)

env.AuthServer.ListManagementKeysCalls.Returns(&authv1.ListManagementKeysResponse{
Items: []*authv1.ManagementKey{
{Id: "key-uuid-1", Prefix: "abc123"},
{Id: "key-uuid-2", Prefix: "def456"},
},
}, nil)

stdout, _, err := testutil.Exec(t, env, "__complete", "iam", "key", "delete", "")
require.NoError(t, err)
assert.Contains(t, stdout, "key-uuid-1")
assert.Contains(t, stdout, "abc123")
assert.Contains(t, stdout, "key-uuid-2")
assert.Contains(t, stdout, "def456")
}
63 changes: 63 additions & 0 deletions internal/cmd/iam/key_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package iam

import (
"io"

"github.com/spf13/cobra"

authv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/auth/v1"

"github.com/qdrant/qcloud-cli/internal/cmd/base"
"github.com/qdrant/qcloud-cli/internal/cmd/output"
"github.com/qdrant/qcloud-cli/internal/state"
)

func newKeyListCommand(s *state.State) *cobra.Command {
return base.ListCmd[*authv1.ListManagementKeysResponse]{
Use: "list",
Short: "List cloud management keys",
Long: `List all cloud management keys for the account.

Management keys grant access to the Qdrant Cloud API and are used to authenticate CLI
and API requests. Each key is identified by its ID and a prefix — the prefix represents
the first bytes of the key value and is safe to display.`,
Example: `# List all management keys for the account
qcloud iam key list

# Output as JSON
qcloud iam key list --json`,
Fetch: func(s *state.State, cmd *cobra.Command) (*authv1.ListManagementKeysResponse, error) {
ctx := cmd.Context()
client, err := s.Client(ctx)
if err != nil {
return nil, err
}

accountID, err := s.AccountID()
if err != nil {
return nil, err
}

return client.Auth().ListManagementKeys(ctx, &authv1.ListManagementKeysRequest{
AccountId: accountID,
})
},
PrintText: func(_ *cobra.Command, w io.Writer, resp *authv1.ListManagementKeysResponse) error {
t := output.NewTable[*authv1.ManagementKey](w)
t.AddField("ID", func(v *authv1.ManagementKey) string {
return v.GetId()
})
t.AddField("PREFIX", func(v *authv1.ManagementKey) string {
return v.GetPrefix()
})
t.AddField("CREATED", func(v *authv1.ManagementKey) string {
if v.GetCreatedAt() != nil {
return output.HumanTime(v.GetCreatedAt().AsTime())
}
return ""
})
t.Write(resp.GetItems())
return nil
},
}.CobraCommand(s)
}
Loading
Loading