Skip to content

Commit e1e2fd0

Browse files
committed
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.
1 parent 0df5875 commit e1e2fd0

File tree

9 files changed

+471
-0
lines changed

9 files changed

+471
-0
lines changed

internal/cli/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/spf13/cobra"
77

8+
"github.com/qdrant/qcloud-cli/internal/cmd/access"
89
"github.com/qdrant/qcloud-cli/internal/cmd/backup"
910
"github.com/qdrant/qcloud-cli/internal/cmd/cloudprovider"
1011
"github.com/qdrant/qcloud-cli/internal/cmd/cloudregion"
@@ -65,6 +66,7 @@ Documentation: https://github.com/qdrant/qcloud-cli`,
6566
s.Config.BindPFlag(config.KeyEndpoint, cmd.PersistentFlags().Lookup("endpoint"))
6667

6768
cmd.AddCommand(version.NewCommand(s))
69+
cmd.AddCommand(access.NewCommand(s))
6870
cmd.AddCommand(cluster.NewCommand(s))
6971
cmd.AddCommand(cloudprovider.NewCommand(s))
7072
cmd.AddCommand(cloudregion.NewCommand(s))

internal/cmd/access/access.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package access
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
"github.com/qdrant/qcloud-cli/internal/state"
7+
)
8+
9+
// NewCommand creates the access command group.
10+
func NewCommand(s *state.State) *cobra.Command {
11+
cmd := &cobra.Command{
12+
Use: "access",
13+
Short: "Manage access to Qdrant Cloud",
14+
Long: `Manage access settings for the Qdrant Cloud account.`,
15+
Args: cobra.NoArgs,
16+
}
17+
cmd.AddCommand(newKeyCommand(s))
18+
return cmd
19+
}

internal/cmd/access/key.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package access
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/access/key_create.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package access
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 access key create
24+
25+
# Create and capture the key value in a script
26+
qcloud access 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 access_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, "access", "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, "access", "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, "access", "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/access/key_delete.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package access
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 access key delete a1b2c3d4-e5f6-7890-abcd-ef1234567890
26+
27+
# Delete without confirmation
28+
qcloud access 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 access_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, "access", "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, "access", "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, "access", "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, "access", "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", "access", "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+
}

0 commit comments

Comments
 (0)