Skip to content

Commit 4227078

Browse files
authored
feat(cli): organization membership (#567)
Signed-off-by: Miguel Martinez Trivino <[email protected]>
1 parent c1d2a3a commit 4227078

13 files changed

+354
-16
lines changed

app/cli/cmd/organization.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ func newOrganizationCmd() *cobra.Command {
3535
newOrganizationDescribeCmd(),
3636
newOrganizationInvitationCmd(),
3737
newOrganizationAPITokenCmd(),
38+
newOrganizationMemberCmd(),
3839
)
3940
return cmd
4041
}

app/cli/cmd/organization_leave.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import (
2424
)
2525

2626
// Get the membership entry associated to the current user for the given organization
27-
func membershipFromOrg(orgID string) (*action.MembershipItem, error) {
28-
memberships, err := action.NewMembershipList(actionOpts).Run()
27+
func membershipFromOrg(ctx context.Context, orgID string) (*action.MembershipItem, error) {
28+
memberships, err := action.NewMembershipList(actionOpts).ListOrgs(ctx)
2929
if err != nil {
3030
return nil, fmt.Errorf("listing memberships: %w", err)
3131
}
@@ -45,8 +45,9 @@ func newOrganizationLeaveCmd() *cobra.Command {
4545
Use: "leave",
4646
Short: "leave an organization",
4747
RunE: func(cmd *cobra.Command, args []string) error {
48+
ctx := cmd.Context()
4849
// To find the membership ID, we need to iterate and filter by org
49-
membership, err := membershipFromOrg(orgID)
50+
membership, err := membershipFromOrg(ctx, orgID)
5051
if err != nil {
5152
return fmt.Errorf("getting membership: %w", err)
5253
} else if membership == nil {
@@ -61,7 +62,7 @@ func newOrganizationLeaveCmd() *cobra.Command {
6162
}
6263

6364
// Membership deletion
64-
if err := action.NewMembershipDelete(actionOpts).Run(context.Background(), membership.ID); err != nil {
65+
if err := action.NewMembershipLeave(actionOpts).Run(ctx, membership.ID); err != nil {
6566
return fmt.Errorf("deleting membership: %w", err)
6667
}
6768

app/cli/cmd/organization_list.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func newOrganizationList() *cobra.Command {
3030
Aliases: []string{"ls"},
3131
Short: "List the organizations this user has access to",
3232
RunE: func(cmd *cobra.Command, args []string) error {
33-
res, err := action.NewMembershipList(actionOpts).Run()
33+
res, err := action.NewMembershipList(actionOpts).ListOrgs(cmd.Context())
3434
if err != nil {
3535
return err
3636
}

app/cli/cmd/organization_member.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// Copyright 2024 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package cmd
17+
18+
import (
19+
"github.com/spf13/cobra"
20+
)
21+
22+
func newOrganizationMemberCmd() *cobra.Command {
23+
cmd := &cobra.Command{
24+
Use: "member",
25+
Short: "Organization members management",
26+
}
27+
28+
cmd.AddCommand(
29+
newOrganizationMemberList(),
30+
newOrganizationMemberUpdateCmd(),
31+
newOrganizationMemberDeleteCmd(),
32+
newOrganizationInvitationCmd(),
33+
)
34+
35+
return cmd
36+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//
2+
// Copyright 2024 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package cmd
17+
18+
import (
19+
"context"
20+
"fmt"
21+
22+
"github.com/chainloop-dev/chainloop/app/cli/internal/action"
23+
"github.com/spf13/cobra"
24+
)
25+
26+
// Get the membership entry associated to the current user for the given organization
27+
func loadMembershipCurrentOrg(ctx context.Context, membershipID string) (*action.MembershipItem, error) {
28+
memberships, err := action.NewMembershipList(actionOpts).ListMembers(ctx)
29+
if err != nil {
30+
return nil, fmt.Errorf("listing memberships: %w", err)
31+
}
32+
33+
for _, m := range memberships {
34+
if m.ID == membershipID {
35+
return m, nil
36+
}
37+
}
38+
39+
return nil, fmt.Errorf("membership %s not found", membershipID)
40+
}
41+
42+
func newOrganizationMemberDeleteCmd() *cobra.Command {
43+
var membershipID string
44+
45+
cmd := &cobra.Command{
46+
Use: "delete",
47+
Short: "Remove a member from the current organization",
48+
RunE: func(cmd *cobra.Command, args []string) error {
49+
ctx := cmd.Context()
50+
m, err := loadMembershipCurrentOrg(ctx, membershipID)
51+
if err != nil {
52+
return fmt.Errorf("getting membership: %w", err)
53+
}
54+
55+
fmt.Printf("You are about to remove the user %q from the organization %q\n", m.User.Email, m.Org.Name)
56+
57+
// Ask for confirmation
58+
if err := confirmDeletion(); err != nil {
59+
return err
60+
}
61+
62+
if err := action.NewMembershipDelete(actionOpts).Run(ctx, membershipID); err != nil {
63+
return err
64+
}
65+
66+
logger.Info().Msg("Member deleted")
67+
return nil
68+
},
69+
}
70+
71+
cmd.Flags().StringVar(&membershipID, "id", "", "Membership ID")
72+
err := cmd.MarkFlagRequired("id")
73+
cobra.CheckErr(err)
74+
75+
return cmd
76+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//
2+
// Copyright 2024 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package cmd
17+
18+
import (
19+
"fmt"
20+
"time"
21+
22+
"github.com/chainloop-dev/chainloop/app/cli/internal/action"
23+
"github.com/jedib0t/go-pretty/v6/table"
24+
"github.com/spf13/cobra"
25+
)
26+
27+
func newOrganizationMemberList() *cobra.Command {
28+
cmd := &cobra.Command{
29+
Use: "list",
30+
Aliases: []string{"ls"},
31+
Short: "List the members of the current organization",
32+
RunE: func(cmd *cobra.Command, args []string) error {
33+
res, err := action.NewMembershipList(actionOpts).ListMembers(cmd.Context())
34+
if err != nil {
35+
return err
36+
}
37+
38+
return encodeOutput(res, orgMembershipsTableOutput)
39+
},
40+
}
41+
42+
return cmd
43+
}
44+
45+
func orgMembershipsTableOutput(items []*action.MembershipItem) error {
46+
if len(items) == 0 {
47+
fmt.Println("you have no access to any organization yet")
48+
return nil
49+
}
50+
51+
t := newTableWriter()
52+
t.AppendHeader(table.Row{"ID", "Email", "Role", "Joined At"})
53+
54+
for _, i := range items {
55+
t.AppendRow(table.Row{i.ID, i.User.Email, i.Role, i.CreatedAt.Format(time.RFC822)})
56+
t.AppendSeparator()
57+
}
58+
59+
t.Render()
60+
return nil
61+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//
2+
// Copyright 2024 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package cmd
17+
18+
import (
19+
"fmt"
20+
21+
"github.com/chainloop-dev/chainloop/app/cli/internal/action"
22+
"github.com/spf13/cobra"
23+
)
24+
25+
func newOrganizationMemberUpdateCmd() *cobra.Command {
26+
var membershipID, role string
27+
28+
cmd := &cobra.Command{
29+
Use: "update",
30+
Short: "Update a member of the organization",
31+
RunE: func(cmd *cobra.Command, args []string) error {
32+
var found bool
33+
for _, r := range action.AvailableRoles {
34+
if string(r) == role {
35+
found = true
36+
break
37+
}
38+
}
39+
40+
if !found {
41+
return fmt.Errorf("role %q is not available. Available roles are %s", role, action.AvailableRoles)
42+
}
43+
44+
res, err := action.NewMembershipUpdate(actionOpts).ChangeRole(cmd.Context(), membershipID, role)
45+
if err != nil {
46+
return err
47+
}
48+
49+
return encodeOutput([]*action.MembershipItem{res}, orgMembershipsTableOutput)
50+
},
51+
}
52+
53+
cmd.Flags().StringVar(&membershipID, "id", "", "Membership ID")
54+
err := cmd.MarkFlagRequired("id")
55+
cobra.CheckErr(err)
56+
57+
cmd.Flags().StringVar(&role, "role", string(action.RoleViewer), fmt.Sprintf("Role of the user in the organization, available %s", action.AvailableRoles))
58+
cobra.CheckErr(err)
59+
60+
return cmd
61+
}

app/cli/cmd/organization_set.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,16 @@ func newOrganizationSet() *cobra.Command {
2929
Use: "set",
3030
Short: "Set the current organization associated with this user",
3131
RunE: func(cmd *cobra.Command, args []string) error {
32+
ctx := cmd.Context()
3233
// To find the membership ID, we need to iterate and filter by org
33-
membership, err := membershipFromOrg(orgID)
34+
membership, err := membershipFromOrg(ctx, orgID)
3435
if err != nil {
3536
return fmt.Errorf("getting membership: %w", err)
3637
} else if membership == nil {
3738
return fmt.Errorf("organization %s not found", orgID)
3839
}
3940

40-
m, err := action.NewMembershipSet(actionOpts).Run(membership.ID)
41+
m, err := action.NewMembershipSet(actionOpts).Run(ctx, membership.ID)
4142
if err != nil {
4243
return err
4344
}

app/cli/internal/action/membership_delete.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2024 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -17,7 +17,6 @@ package action
1717

1818
import (
1919
"context"
20-
"fmt"
2120

2221
pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
2322
)
@@ -31,9 +30,9 @@ func NewMembershipDelete(cfg *ActionsOpts) *MembershipDelete {
3130
}
3231

3332
func (action *MembershipDelete) Run(ctx context.Context, membershipID string) error {
34-
client := pb.NewUserServiceClient(action.cfg.CPConnection)
35-
if _, err := client.DeleteMembership(ctx, &pb.DeleteMembershipRequest{MembershipId: membershipID}); err != nil {
36-
return fmt.Errorf("deleting membership: %w", err)
33+
client := pb.NewOrganizationServiceClient(action.cfg.CPConnection)
34+
if _, err := client.DeleteMembership(ctx, &pb.OrganizationServiceDeleteMembershipRequest{MembershipId: membershipID}); err != nil {
35+
return err
3736
}
3837

3938
return nil
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//
2+
// Copyright 2023 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package action
17+
18+
import (
19+
"context"
20+
21+
pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
22+
)
23+
24+
type MembershipLeave struct {
25+
cfg *ActionsOpts
26+
}
27+
28+
func NewMembershipLeave(cfg *ActionsOpts) *MembershipLeave {
29+
return &MembershipLeave{cfg}
30+
}
31+
32+
func (action *MembershipLeave) Run(ctx context.Context, membershipID string) error {
33+
client := pb.NewUserServiceClient(action.cfg.CPConnection)
34+
if _, err := client.DeleteMembership(ctx, &pb.DeleteMembershipRequest{MembershipId: membershipID}); err != nil {
35+
return err
36+
}
37+
38+
return nil
39+
}

0 commit comments

Comments
 (0)