Skip to content

Commit 17edf2f

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/dhernando/output-table-pattern-in-list-cmds
2 parents a8294db + 6e83875 commit 17edf2f

38 files changed

Lines changed: 2908 additions & 6 deletions

internal/cmd/completion/iam.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package completion
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
iamv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/iam/v1"
7+
8+
"github.com/qdrant/qcloud-cli/internal/state"
9+
)
10+
11+
// RoleCompletion returns a completion function that completes IAM role names
12+
// with their ID as description.
13+
func RoleCompletion(s *state.State) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
14+
return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
15+
ctx := cmd.Context()
16+
client, err := s.Client(ctx)
17+
if err != nil {
18+
return nil, cobra.ShellCompDirectiveError
19+
}
20+
21+
accountID, err := s.AccountID()
22+
if err != nil {
23+
return nil, cobra.ShellCompDirectiveError
24+
}
25+
26+
resp, err := client.IAM().ListRoles(ctx, &iamv1.ListRolesRequest{AccountId: accountID})
27+
if err != nil {
28+
return nil, cobra.ShellCompDirectiveError
29+
}
30+
31+
completions := make([]string, 0, len(resp.GetItems()))
32+
for _, r := range resp.GetItems() {
33+
completions = append(completions, r.GetName()+"\t"+r.GetId())
34+
}
35+
return completions, cobra.ShellCompDirectiveNoFileComp
36+
}
37+
}
38+
39+
// RoleIDCompletion returns a ValidArgsFunction that completes role IDs.
40+
func RoleIDCompletion(s *state.State) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
41+
return func(cmd *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
42+
if len(args) > 0 {
43+
return nil, cobra.ShellCompDirectiveNoFileComp
44+
}
45+
46+
ctx := cmd.Context()
47+
client, err := s.Client(ctx)
48+
if err != nil {
49+
return nil, cobra.ShellCompDirectiveError
50+
}
51+
52+
accountID, err := s.AccountID()
53+
if err != nil {
54+
return nil, cobra.ShellCompDirectiveError
55+
}
56+
57+
resp, err := client.IAM().ListRoles(ctx, &iamv1.ListRolesRequest{
58+
AccountId: accountID,
59+
})
60+
if err != nil {
61+
return nil, cobra.ShellCompDirectiveError
62+
}
63+
64+
completions := make([]string, 0, len(resp.GetItems()))
65+
for _, r := range resp.GetItems() {
66+
completions = append(completions, r.GetId()+"\t"+r.GetName())
67+
}
68+
return completions, cobra.ShellCompDirectiveNoFileComp
69+
}
70+
}
71+
72+
// PermissionCompletion returns a completion function for the --permission flag.
73+
func PermissionCompletion(s *state.State) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
74+
return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
75+
ctx := cmd.Context()
76+
client, err := s.Client(ctx)
77+
if err != nil {
78+
return nil, cobra.ShellCompDirectiveError
79+
}
80+
81+
accountID, err := s.AccountID()
82+
if err != nil {
83+
return nil, cobra.ShellCompDirectiveError
84+
}
85+
86+
resp, err := client.IAM().ListPermissions(ctx, &iamv1.ListPermissionsRequest{
87+
AccountId: accountID,
88+
})
89+
if err != nil {
90+
return nil, cobra.ShellCompDirectiveError
91+
}
92+
93+
completions := make([]string, 0, len(resp.GetPermissions()))
94+
for _, p := range resp.GetPermissions() {
95+
completions = append(completions, p.GetValue()+"\t"+p.GetCategory())
96+
}
97+
return completions, cobra.ShellCompDirectiveNoFileComp
98+
}
99+
}

internal/cmd/iam/completion.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package iam
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
iamv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/iam/v1"
7+
8+
"github.com/qdrant/qcloud-cli/internal/state"
9+
)
10+
11+
// userCompletion returns a ValidArgsFunction that completes user IDs with
12+
// their email as description. It only completes the first positional argument.
13+
func userCompletion(s *state.State) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
14+
return func(cmd *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
15+
if len(args) > 0 {
16+
return nil, cobra.ShellCompDirectiveNoFileComp
17+
}
18+
ctx := cmd.Context()
19+
client, err := s.Client(ctx)
20+
if err != nil {
21+
return nil, cobra.ShellCompDirectiveError
22+
}
23+
accountID, err := s.AccountID()
24+
if err != nil {
25+
return nil, cobra.ShellCompDirectiveError
26+
}
27+
28+
resp, err := client.IAM().ListUsers(ctx, &iamv1.ListUsersRequest{AccountId: accountID})
29+
if err != nil {
30+
return nil, cobra.ShellCompDirectiveError
31+
}
32+
33+
completions := make([]string, 0, len(resp.GetItems()))
34+
for _, u := range resp.GetItems() {
35+
completions = append(completions, u.GetId()+"\t"+u.GetEmail())
36+
}
37+
return completions, cobra.ShellCompDirectiveNoFileComp
38+
}
39+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package iam_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
9+
iamv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/iam/v1"
10+
11+
"github.com/qdrant/qcloud-cli/internal/testutil"
12+
)
13+
14+
func TestUserCompletion(t *testing.T) {
15+
env := testutil.NewTestEnv(t)
16+
17+
env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{
18+
Items: []*iamv1.User{
19+
{Id: "user-uuid-1", Email: "alice@example.com"},
20+
{Id: "user-uuid-2", Email: "bob@example.com"},
21+
},
22+
}, nil)
23+
24+
stdout, _, err := testutil.Exec(t, env, "__complete", "iam", "user", "describe", "")
25+
require.NoError(t, err)
26+
assert.Contains(t, stdout, "user-uuid-1")
27+
assert.Contains(t, stdout, "alice@example.com")
28+
assert.Contains(t, stdout, "user-uuid-2")
29+
assert.Contains(t, stdout, "bob@example.com")
30+
}
31+
32+
func TestUserCompletion_StopsAfterFirstArg(t *testing.T) {
33+
env := testutil.NewTestEnv(t)
34+
35+
stdout, _, err := testutil.Exec(t, env, "__complete", "iam", "user", "describe", "user-uuid-1", "")
36+
require.NoError(t, err)
37+
assert.NotContains(t, stdout, "user-uuid")
38+
}
39+
40+
func TestUserThenRoleCompletion_FirstArg(t *testing.T) {
41+
env := testutil.NewTestEnv(t)
42+
43+
env.IAMServer.ListUsersCalls.Returns(&iamv1.ListUsersResponse{
44+
Items: []*iamv1.User{
45+
{Id: "user-uuid-1", Email: "alice@example.com"},
46+
},
47+
}, nil)
48+
49+
stdout, _, err := testutil.Exec(t, env, "__complete", "iam", "user", "assign-role", "")
50+
require.NoError(t, err)
51+
assert.Contains(t, stdout, "user-uuid-1")
52+
assert.Contains(t, stdout, "alice@example.com")
53+
}
54+
55+
func TestRoleIDCompletion_Describe(t *testing.T) {
56+
env := testutil.NewTestEnv(t)
57+
env.IAMServer.ListRolesCalls.Returns(&iamv1.ListRolesResponse{
58+
Items: []*iamv1.Role{
59+
{Id: "role-uuid-1", Name: "Admin"},
60+
{Id: "role-uuid-2", Name: "Viewer"},
61+
},
62+
}, nil)
63+
64+
stdout, _, err := testutil.Exec(t, env, "__complete", "iam", "role", "describe", "")
65+
require.NoError(t, err)
66+
assert.Contains(t, stdout, "role-uuid-1")
67+
assert.Contains(t, stdout, "Admin")
68+
assert.Contains(t, stdout, "role-uuid-2")
69+
assert.Contains(t, stdout, "Viewer")
70+
}
71+
72+
func TestRoleIDCompletion_Delete(t *testing.T) {
73+
env := testutil.NewTestEnv(t)
74+
env.IAMServer.ListRolesCalls.Returns(&iamv1.ListRolesResponse{
75+
Items: []*iamv1.Role{
76+
{Id: "role-uuid-1", Name: "Admin"},
77+
},
78+
}, nil)
79+
80+
stdout, _, err := testutil.Exec(t, env, "__complete", "iam", "role", "delete", "")
81+
require.NoError(t, err)
82+
assert.Contains(t, stdout, "role-uuid-1")
83+
assert.Contains(t, stdout, "Admin")
84+
}
85+
86+
func TestRoleIDCompletion_AssignPermission(t *testing.T) {
87+
env := testutil.NewTestEnv(t)
88+
env.IAMServer.ListRolesCalls.Returns(&iamv1.ListRolesResponse{
89+
Items: []*iamv1.Role{
90+
{Id: "role-uuid-1", Name: "Custom Role"},
91+
},
92+
}, nil)
93+
94+
stdout, _, err := testutil.Exec(t, env, "__complete", "iam", "role", "assign-permission", "")
95+
require.NoError(t, err)
96+
assert.Contains(t, stdout, "role-uuid-1")
97+
assert.Contains(t, stdout, "Custom Role")
98+
}
99+
100+
func TestPermissionCompletion_Create(t *testing.T) {
101+
env := testutil.NewTestEnv(t)
102+
env.IAMServer.ListPermissionsCalls.Returns(&iamv1.ListPermissionsResponse{
103+
Permissions: []*iamv1.Permission{
104+
{Value: "read:clusters", Category: new("Cluster")},
105+
{Value: "write:backups", Category: new("Backup")},
106+
},
107+
}, nil)
108+
109+
stdout, _, err := testutil.Exec(t, env, "__complete", "iam", "role", "create", "--name", "test", "--permission", "")
110+
require.NoError(t, err)
111+
assert.Contains(t, stdout, "read:clusters")
112+
assert.Contains(t, stdout, "Cluster")
113+
assert.Contains(t, stdout, "write:backups")
114+
assert.Contains(t, stdout, "Backup")
115+
}
116+
117+
func TestPermissionCompletion_AssignPermission(t *testing.T) {
118+
env := testutil.NewTestEnv(t)
119+
env.IAMServer.ListPermissionsCalls.Returns(&iamv1.ListPermissionsResponse{
120+
Permissions: []*iamv1.Permission{
121+
{Value: "read:clusters", Category: new("Cluster")},
122+
},
123+
}, nil)
124+
125+
stdout, _, err := testutil.Exec(t, env, "__complete", "iam", "role", "assign-permission", "some-role-id", "--permission", "")
126+
require.NoError(t, err)
127+
assert.Contains(t, stdout, "read:clusters")
128+
assert.Contains(t, stdout, "Cluster")
129+
}
130+
131+
func TestPermissionCompletion_RemovePermission(t *testing.T) {
132+
env := testutil.NewTestEnv(t)
133+
env.IAMServer.ListPermissionsCalls.Returns(&iamv1.ListPermissionsResponse{
134+
Permissions: []*iamv1.Permission{
135+
{Value: "write:backups", Category: new("Backup")},
136+
},
137+
}, nil)
138+
139+
stdout, _, err := testutil.Exec(t, env, "__complete", "iam", "role", "remove-permission", "some-role-id", "--permission", "")
140+
require.NoError(t, err)
141+
assert.Contains(t, stdout, "write:backups")
142+
assert.Contains(t, stdout, "Backup")
143+
}

internal/cmd/iam/iam.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ 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))
17+
cmd.AddCommand(
18+
newKeyCommand(s),
19+
newUserCommand(s),
20+
newRoleCommand(s),
21+
newPermissionCommand(s),
22+
)
1823
return cmd
1924
}

internal/cmd/iam/iam_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package iam_test
2+
3+
// Shared test constants used across iam subcommand test files.
4+
const (
5+
testUserID = "7b2ea926-724b-4de2-b73a-8675c42a6ebe"
6+
testRoleID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
7+
)

internal/cmd/iam/permission.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package iam
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
"github.com/qdrant/qcloud-cli/internal/state"
7+
)
8+
9+
func newPermissionCommand(s *state.State) *cobra.Command {
10+
cmd := &cobra.Command{
11+
Use: "permission",
12+
Short: "Manage permissions in Qdrant Cloud",
13+
Long: `Manage permissions for the Qdrant Cloud account.
14+
15+
Permissions represent individual access rights that can be assigned to roles.
16+
Use these commands to discover which permissions are available in the system.`,
17+
Args: cobra.NoArgs,
18+
}
19+
cmd.AddCommand(newPermissionListCommand(s))
20+
return cmd
21+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package iam
2+
3+
import (
4+
"io"
5+
6+
"github.com/spf13/cobra"
7+
8+
iamv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/iam/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 newPermissionListCommand(s *state.State) *cobra.Command {
16+
return base.ListCmd[*iamv1.ListPermissionsResponse]{
17+
Use: "list",
18+
Short: "List all available permissions",
19+
Long: `List all permissions known in the system for the account.
20+
21+
Permissions are the individual access rights that can be assigned to roles.
22+
Each permission has a value (e.g. "read:clusters") and a category
23+
(e.g. "Cluster").`,
24+
Example: `# List all available permissions
25+
qcloud iam permission list
26+
27+
# Output as JSON
28+
qcloud iam permission list --json`,
29+
Fetch: func(s *state.State, cmd *cobra.Command) (*iamv1.ListPermissionsResponse, 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.IAM().ListPermissions(ctx, &iamv1.ListPermissionsRequest{
42+
AccountId: accountID,
43+
})
44+
},
45+
PrintText: func(_ *cobra.Command, w io.Writer, resp *iamv1.ListPermissionsResponse) error {
46+
t := output.NewTable[*iamv1.Permission](w)
47+
t.AddField("PERMISSION", func(v *iamv1.Permission) string {
48+
return v.GetValue()
49+
})
50+
t.AddField("CATEGORY", func(v *iamv1.Permission) string {
51+
return v.GetCategory()
52+
})
53+
t.Write(resp.GetPermissions())
54+
return nil
55+
},
56+
}.CobraCommand(s)
57+
}

0 commit comments

Comments
 (0)