Skip to content

Commit 8a31dbf

Browse files
authored
feat: management api key commands (#98)
* feat: add access command group for qdrant cloud * feat: add access key commands for qdrant cloud auth management Adds `qcloud access key list`, `create`, and `delete` subcommands for managing Qdrant Cloud auth access keys. * refactor: access -> iam
1 parent d8ba9fa commit 8a31dbf

8 files changed

Lines changed: 451 additions & 0 deletions

File tree

internal/cmd/iam/iam.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ func NewCommand(s *state.State) *cobra.Command {
1414
Long: `Manage IAM resources for the Qdrant Cloud account.`,
1515
Args: cobra.NoArgs,
1616
}
17+
cmd.AddCommand(newKeyCommand(s))
1718
return cmd
1819
}

internal/cmd/iam/key.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package iam
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
"github.com/qdrant/qcloud-cli/internal/state"
7+
)
8+
9+
func newKeyCommand(s *state.State) *cobra.Command {
10+
cmd := &cobra.Command{
11+
Use: "key",
12+
Short: "Manage cloud management keys",
13+
Long: `Manage cloud management keys for the account.
14+
15+
Management keys authenticate requests to the Qdrant Cloud API. Use them to authorize
16+
the CLI, automation scripts, or any other tooling that calls the Qdrant Cloud API.`,
17+
Args: cobra.NoArgs,
18+
}
19+
cmd.AddCommand(
20+
newKeyListCommand(s),
21+
newKeyCreateCommand(s),
22+
newKeyDeleteCommand(s),
23+
)
24+
return cmd
25+
}

internal/cmd/iam/key_create.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package iam
2+
3+
import (
4+
"fmt"
5+
"io"
6+
7+
"github.com/spf13/cobra"
8+
9+
authv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/auth/v1"
10+
11+
"github.com/qdrant/qcloud-cli/internal/cmd/base"
12+
"github.com/qdrant/qcloud-cli/internal/state"
13+
)
14+
15+
func newKeyCreateCommand(s *state.State) *cobra.Command {
16+
return base.CreateCmd[*authv1.ManagementKey]{
17+
Long: `Create a new cloud management key for the account.
18+
19+
Management keys grant access to the Qdrant Cloud API. The full key value is returned
20+
only once at creation time — store it securely, as it cannot be retrieved again. If a
21+
key is lost, delete it and create a new one.`,
22+
Example: `# Create a new management key
23+
qcloud iam key create
24+
25+
# Create and capture the key value in a script
26+
qcloud iam key create --json | jq -r '.key'`,
27+
BaseCobraCommand: func() *cobra.Command {
28+
return &cobra.Command{
29+
Use: "create",
30+
Short: "Create a cloud management key",
31+
Args: cobra.NoArgs,
32+
}
33+
},
34+
Run: func(s *state.State, cmd *cobra.Command, args []string) (*authv1.ManagementKey, error) {
35+
ctx := cmd.Context()
36+
client, err := s.Client(ctx)
37+
if err != nil {
38+
return nil, err
39+
}
40+
41+
accountID, err := s.AccountID()
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
resp, err := client.Auth().CreateManagementKey(ctx, &authv1.CreateManagementKeyRequest{
47+
ManagementKey: &authv1.ManagementKey{
48+
AccountId: accountID,
49+
},
50+
})
51+
if err != nil {
52+
return nil, fmt.Errorf("failed to create management key: %w", err)
53+
}
54+
55+
return resp.GetManagementKey(), nil
56+
},
57+
PrintResource: func(_ *cobra.Command, out io.Writer, key *authv1.ManagementKey) {
58+
fmt.Fprintf(out, "Management key %s created.\n", key.GetId())
59+
if k := key.GetKey(); k != "" {
60+
fmt.Fprintln(out, "")
61+
fmt.Fprintln(out, "Save this key now — it will not be shown again:")
62+
fmt.Fprintf(out, " %s\n", k)
63+
}
64+
},
65+
}.CobraCommand(s)
66+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package iam_test
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
11+
authv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/auth/v1"
12+
13+
"github.com/qdrant/qcloud-cli/internal/testutil"
14+
)
15+
16+
func TestKeyCreate_PrintsIDAndKey(t *testing.T) {
17+
env := testutil.NewTestEnv(t, testutil.WithAccountID("test-account-id"))
18+
19+
env.AuthServer.CreateManagementKeyCalls.Returns(&authv1.CreateManagementKeyResponse{
20+
ManagementKey: &authv1.ManagementKey{
21+
Id: "new-key-id",
22+
Key: "super-secret-value",
23+
},
24+
}, nil)
25+
26+
stdout, _, err := testutil.Exec(t, env, "iam", "key", "create")
27+
require.NoError(t, err)
28+
assert.Contains(t, stdout, "new-key-id")
29+
assert.Contains(t, stdout, "super-secret-value")
30+
assert.Contains(t, stdout, "Save this key now")
31+
32+
req, ok := env.AuthServer.CreateManagementKeyCalls.Last()
33+
require.True(t, ok)
34+
assert.Equal(t, "test-account-id", req.GetManagementKey().GetAccountId())
35+
}
36+
37+
func TestKeyCreate_BackendError(t *testing.T) {
38+
env := testutil.NewTestEnv(t)
39+
40+
env.AuthServer.CreateManagementKeyCalls.Returns(nil, fmt.Errorf("internal server error"))
41+
42+
_, _, err := testutil.Exec(t, env, "iam", "key", "create")
43+
require.Error(t, err)
44+
}
45+
46+
func TestKeyCreate_JSONOutput(t *testing.T) {
47+
env := testutil.NewTestEnv(t)
48+
49+
env.AuthServer.CreateManagementKeyCalls.Returns(&authv1.CreateManagementKeyResponse{
50+
ManagementKey: &authv1.ManagementKey{
51+
Id: "json-key-id",
52+
Key: "secret",
53+
},
54+
}, nil)
55+
56+
stdout, _, err := testutil.Exec(t, env, "iam", "key", "create", "--json")
57+
require.NoError(t, err)
58+
59+
var result struct {
60+
ID string `json:"id"`
61+
Key string `json:"key"`
62+
}
63+
require.NoError(t, json.Unmarshal([]byte(stdout), &result))
64+
assert.Equal(t, "json-key-id", result.ID)
65+
assert.Equal(t, "secret", result.Key)
66+
}

internal/cmd/iam/key_delete.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package iam
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
8+
authv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/auth/v1"
9+
10+
"github.com/qdrant/qcloud-cli/internal/cmd/base"
11+
"github.com/qdrant/qcloud-cli/internal/cmd/completion"
12+
"github.com/qdrant/qcloud-cli/internal/cmd/util"
13+
"github.com/qdrant/qcloud-cli/internal/state"
14+
)
15+
16+
func newKeyDeleteCommand(s *state.State) *cobra.Command {
17+
return base.Cmd{
18+
Long: `Delete a cloud management key from the account.
19+
20+
Deleting a key immediately revokes its access to the Qdrant Cloud API. Any client
21+
using the deleted key will receive authentication errors. This action cannot be undone.
22+
23+
A confirmation prompt is shown unless --force is passed.`,
24+
Example: `# Delete a management key (with confirmation prompt)
25+
qcloud iam key delete a1b2c3d4-e5f6-7890-abcd-ef1234567890
26+
27+
# Delete without confirmation
28+
qcloud iam key delete a1b2c3d4-e5f6-7890-abcd-ef1234567890 --force`,
29+
BaseCobraCommand: func() *cobra.Command {
30+
cmd := &cobra.Command{
31+
Use: "delete <key-id>",
32+
Short: "Delete a cloud management key",
33+
Args: util.ExactArgs(1, "a management key ID"),
34+
}
35+
cmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
36+
return cmd
37+
},
38+
ValidArgsFunction: completion.ManagementKeyIDCompletion(s),
39+
Run: func(s *state.State, cmd *cobra.Command, args []string) error {
40+
keyID := args[0]
41+
42+
force, _ := cmd.Flags().GetBool("force")
43+
if !util.ConfirmAction(force, cmd.ErrOrStderr(), fmt.Sprintf("Are you sure you want to delete management key %s?", keyID)) {
44+
fmt.Fprintln(cmd.OutOrStdout(), "Aborted.")
45+
return nil
46+
}
47+
48+
ctx := cmd.Context()
49+
client, err := s.Client(ctx)
50+
if err != nil {
51+
return err
52+
}
53+
54+
accountID, err := s.AccountID()
55+
if err != nil {
56+
return err
57+
}
58+
59+
_, err = client.Auth().DeleteManagementKey(ctx, &authv1.DeleteManagementKeyRequest{
60+
AccountId: accountID,
61+
ManagementKeyId: keyID,
62+
})
63+
if err != nil {
64+
return fmt.Errorf("failed to delete management key: %w", err)
65+
}
66+
67+
fmt.Fprintf(cmd.OutOrStdout(), "Management key %s deleted.\n", keyID)
68+
return nil
69+
},
70+
}.CobraCommand(s)
71+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package iam_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
authv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/auth/v1"
11+
12+
"github.com/qdrant/qcloud-cli/internal/testutil"
13+
)
14+
15+
func TestKeyDelete_WithForce(t *testing.T) {
16+
env := testutil.NewTestEnv(t, testutil.WithAccountID("test-account-id"))
17+
18+
env.AuthServer.DeleteManagementKeyCalls.Returns(&authv1.DeleteManagementKeyResponse{}, nil)
19+
20+
stdout, _, err := testutil.Exec(t, env, "iam", "key", "delete", "key-abc", "--force")
21+
require.NoError(t, err)
22+
assert.Contains(t, stdout, "key-abc")
23+
assert.Contains(t, stdout, "deleted")
24+
25+
req, ok := env.AuthServer.DeleteManagementKeyCalls.Last()
26+
require.True(t, ok)
27+
assert.Equal(t, "test-account-id", req.GetAccountId())
28+
assert.Equal(t, "key-abc", req.GetManagementKeyId())
29+
}
30+
31+
func TestKeyDelete_Aborted(t *testing.T) {
32+
env := testutil.NewTestEnv(t)
33+
34+
stdout, _, err := testutil.Exec(t, env, "iam", "key", "delete", "key-abc")
35+
require.NoError(t, err)
36+
assert.Contains(t, stdout, "Aborted.")
37+
assert.Equal(t, 0, env.AuthServer.DeleteManagementKeyCalls.Count())
38+
}
39+
40+
func TestKeyDelete_BackendError(t *testing.T) {
41+
env := testutil.NewTestEnv(t)
42+
43+
env.AuthServer.DeleteManagementKeyCalls.Returns(nil, fmt.Errorf("internal server error"))
44+
45+
_, _, err := testutil.Exec(t, env, "iam", "key", "delete", "key-abc", "--force")
46+
require.Error(t, err)
47+
}
48+
49+
func TestKeyDelete_MissingArg(t *testing.T) {
50+
env := testutil.NewTestEnv(t)
51+
52+
_, _, err := testutil.Exec(t, env, "iam", "key", "delete")
53+
require.Error(t, err)
54+
}
55+
56+
func TestKeyDeleteCompletion(t *testing.T) {
57+
env := testutil.NewTestEnv(t)
58+
59+
env.AuthServer.ListManagementKeysCalls.Returns(&authv1.ListManagementKeysResponse{
60+
Items: []*authv1.ManagementKey{
61+
{Id: "key-uuid-1", Prefix: "abc123"},
62+
{Id: "key-uuid-2", Prefix: "def456"},
63+
},
64+
}, nil)
65+
66+
stdout, _, err := testutil.Exec(t, env, "__complete", "iam", "key", "delete", "")
67+
require.NoError(t, err)
68+
assert.Contains(t, stdout, "key-uuid-1")
69+
assert.Contains(t, stdout, "abc123")
70+
assert.Contains(t, stdout, "key-uuid-2")
71+
assert.Contains(t, stdout, "def456")
72+
}

internal/cmd/iam/key_list.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package iam
2+
3+
import (
4+
"io"
5+
6+
"github.com/spf13/cobra"
7+
8+
authv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/auth/v1"
9+
10+
"github.com/qdrant/qcloud-cli/internal/cmd/base"
11+
"github.com/qdrant/qcloud-cli/internal/cmd/output"
12+
"github.com/qdrant/qcloud-cli/internal/state"
13+
)
14+
15+
func newKeyListCommand(s *state.State) *cobra.Command {
16+
return base.ListCmd[*authv1.ListManagementKeysResponse]{
17+
Use: "list",
18+
Short: "List cloud management keys",
19+
Long: `List all cloud management keys for the account.
20+
21+
Management keys grant access to the Qdrant Cloud API and are used to authenticate CLI
22+
and API requests. Each key is identified by its ID and a prefix — the prefix represents
23+
the first bytes of the key value and is safe to display.`,
24+
Example: `# List all management keys for the account
25+
qcloud iam key list
26+
27+
# Output as JSON
28+
qcloud iam key list --json`,
29+
Fetch: func(s *state.State, cmd *cobra.Command) (*authv1.ListManagementKeysResponse, error) {
30+
ctx := cmd.Context()
31+
client, err := s.Client(ctx)
32+
if err != nil {
33+
return nil, err
34+
}
35+
36+
accountID, err := s.AccountID()
37+
if err != nil {
38+
return nil, err
39+
}
40+
41+
return client.Auth().ListManagementKeys(ctx, &authv1.ListManagementKeysRequest{
42+
AccountId: accountID,
43+
})
44+
},
45+
PrintText: func(_ *cobra.Command, w io.Writer, resp *authv1.ListManagementKeysResponse) error {
46+
t := output.NewTable[*authv1.ManagementKey](w)
47+
t.AddField("ID", func(v *authv1.ManagementKey) string {
48+
return v.GetId()
49+
})
50+
t.AddField("PREFIX", func(v *authv1.ManagementKey) string {
51+
return v.GetPrefix()
52+
})
53+
t.AddField("CREATED", func(v *authv1.ManagementKey) string {
54+
if v.GetCreatedAt() != nil {
55+
return output.HumanTime(v.GetCreatedAt().AsTime())
56+
}
57+
return ""
58+
})
59+
t.Write(resp.GetItems())
60+
return nil
61+
},
62+
}.CobraCommand(s)
63+
}

0 commit comments

Comments
 (0)