Skip to content

Commit 9d9291b

Browse files
authored
Merge pull request #42 from intility/24-add-commands-to-manage-teams
24 add commands to manage teams
2 parents a5b27e1 + 01e461a commit 9d9291b

File tree

7 files changed

+326
-31
lines changed

7 files changed

+326
-31
lines changed

pkg/client/client.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,16 @@ type MeClient interface {
3636
GetMe(ctx context.Context) (Me, error)
3737
}
3838

39+
type TeamsClient interface {
40+
ListTeams(ctx context.Context) ([]Team, error)
41+
GetTeamMembers(ctx context.Context, teamId string) ([]TeamMember, error)
42+
}
43+
3944
type Client interface {
4045
ClusterClient
4146
IntegrationClient
4247
MeClient
48+
TeamsClient
4349
}
4450

4551
type RestClientOption func(*RestClient)

pkg/client/teams.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/google/uuid"
8+
)
9+
10+
type Team struct {
11+
ID string `json:"id"`
12+
Name string `json:"name"`
13+
Description string `json:"description"`
14+
Role []string `json:"roles"`
15+
}
16+
17+
type Subject struct {
18+
Type string `json:"type"`
19+
Name string `json:"name"`
20+
Details string `json:"details"`
21+
ID uuid.UUID `json:"id"`
22+
}
23+
24+
type MemberRole string
25+
26+
const (
27+
MemberRoleOwner MemberRole = "owner"
28+
MemberRoleMember MemberRole = "member"
29+
)
30+
31+
type TeamMember struct {
32+
Subject Subject `json:"subject"`
33+
Roles []MemberRole `json:"roles"`
34+
}
35+
36+
func (c *RestClient) ListTeams(ctx context.Context) ([]Team, error) {
37+
var teams []Team
38+
39+
req, err := c.createAuthenticatedRequest(ctx, "GET", c.baseURI+"/api/v1/teams", nil)
40+
if err != nil {
41+
return teams, err
42+
}
43+
44+
if err = doRequest(c.httpClient, req, &teams); err != nil {
45+
return teams, fmt.Errorf("request failed: %w", err)
46+
}
47+
48+
return teams, nil
49+
}
50+
51+
func (c *RestClient) GetTeamMembers(ctx context.Context, teamId string) ([]TeamMember, error) {
52+
var members []TeamMember
53+
54+
req, err := c.createAuthenticatedRequest(ctx, "GET", c.baseURI+"/api/v1/teams/"+teamId+"/members", nil)
55+
if err != nil {
56+
return members, err
57+
}
58+
59+
if err = doRequest(c.httpClient, req, &members); err != nil {
60+
return members, fmt.Errorf("request failed: %w", err)
61+
}
62+
63+
return members, nil
64+
}

pkg/commands/cluster/list.go

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package cluster
22

33
import (
44
"encoding/json"
5-
"errors"
65
"fmt"
76
"io"
87

@@ -14,12 +13,11 @@ import (
1413
"github.com/intility/indev/internal/ux"
1514
"github.com/intility/indev/pkg/client"
1615
"github.com/intility/indev/pkg/clientset"
16+
"github.com/intility/indev/pkg/outputformat"
1717
)
1818

19-
var errInvalidOutputFormat = errors.New(`must be one of "wide", "json", "yaml"`)
20-
2119
func NewListCommand(set clientset.ClientSet) *cobra.Command {
22-
output := outputFormat("")
20+
output := outputformat.Format("")
2321
// clusterListCmd represents the list command.
2422
cmd := &cobra.Command{
2523
Use: "list",
@@ -55,7 +53,7 @@ func NewListCommand(set clientset.ClientSet) *cobra.Command {
5553
return cmd
5654
}
5755

58-
func printClusterList(writer io.Writer, format outputFormat, clusters client.ClusterList) error {
56+
func printClusterList(writer io.Writer, format outputformat.Format, clusters client.ClusterList) error {
5957
var err error
6058

6159
switch format {
@@ -126,32 +124,6 @@ func statusMessage(cluster client.Cluster) string {
126124
return cluster.Status.Ready.Message
127125
}
128126

129-
type OutputFormat interface {
130-
String() string
131-
Set(val string) error
132-
Type() string
133-
}
134-
135-
type outputFormat string
136-
137-
func (o *outputFormat) String() string {
138-
return string(*o)
139-
}
140-
141-
func (o *outputFormat) Set(value string) error {
142-
switch value {
143-
case "wide", "json", "yaml":
144-
*o = outputFormat(value)
145-
return nil
146-
default:
147-
return errInvalidOutputFormat
148-
}
149-
}
150-
151-
func (o *outputFormat) Type() string {
152-
return "outputFormat"
153-
}
154-
155127
func nodePoolSummary(cluster client.Cluster) string {
156128
if len(cluster.NodePools) == 0 {
157129
return "0"

pkg/commands/teams/get.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package teams
2+
3+
import (
4+
"io"
5+
"slices"
6+
7+
"github.com/spf13/cobra"
8+
9+
"github.com/intility/indev/internal/redact"
10+
"github.com/intility/indev/internal/telemetry"
11+
"github.com/intility/indev/internal/ux"
12+
"github.com/intility/indev/pkg/client"
13+
"github.com/intility/indev/pkg/clientset"
14+
)
15+
16+
func NewGetCommand(set clientset.ClientSet) *cobra.Command {
17+
var (
18+
teamName string
19+
errEmptyName = redact.Errorf("team name cannot be empty")
20+
)
21+
22+
cmd := &cobra.Command{
23+
Use: "get [name]",
24+
Short: "Get detailed information about a team",
25+
Long: `Display comprehensive information about a specific team`,
26+
Args: cobra.MaximumNArgs(1),
27+
PreRunE: set.EnsureSignedInPreHook,
28+
RunE: func(cmd *cobra.Command, args []string) error {
29+
ctx, span := telemetry.StartSpan(cmd.Context(), "team.get")
30+
defer span.End()
31+
32+
cmd.SilenceUsage = true
33+
34+
// If positional argument is provided, use it (takes precedence)
35+
if len(args) > 0 {
36+
teamName = args[0]
37+
}
38+
39+
if teamName == "" {
40+
return errEmptyName
41+
}
42+
43+
// List teams to find the one by name
44+
teams, err := set.PlatformClient.ListTeams(ctx)
45+
if err != nil {
46+
return redact.Errorf("could not get team: %w", redact.Safe(err))
47+
}
48+
49+
// Find the team with the matching name
50+
var team *client.Team
51+
for _, c := range teams {
52+
if c.Name == teamName {
53+
team = &c
54+
break
55+
}
56+
}
57+
58+
if team == nil {
59+
return redact.Errorf("team not found: %s", teamName)
60+
}
61+
62+
members, err := set.PlatformClient.GetTeamMembers(ctx, team.ID)
63+
if err != nil {
64+
return redact.Errorf("could not get members from team: %w", redact.Safe(err))
65+
}
66+
67+
if err = printTeamDetails(cmd.OutOrStdout(), team, members); err != nil {
68+
return redact.Errorf("could not print team details: %w", redact.Safe(err))
69+
}
70+
71+
return nil
72+
},
73+
}
74+
75+
cmd.Flags().StringVarP(&teamName, "name", "n", "", "Name of the team")
76+
77+
return cmd
78+
}
79+
80+
func printTeamDetails(writer io.Writer, team *client.Team, members []client.TeamMember) error {
81+
ux.Fprint(writer, "Team Information:\n")
82+
ux.Fprint(writer, " ID: %s\n", team.ID)
83+
ux.Fprint(writer, " Name: %s\n", team.Name)
84+
ux.Fprint(writer, " Description: %s\n", team.Description)
85+
ux.Fprint(writer, "Members:\n")
86+
87+
table := ux.TableFromObjects(members, func(member client.TeamMember) []ux.Row {
88+
return []ux.Row{
89+
ux.NewRow(" Name", " "+member.Subject.Name),
90+
ux.NewRow(" Role", " "+getTeamRole(member.Roles)),
91+
}
92+
})
93+
94+
ux.Fprint(writer, "%s", table.String())
95+
96+
return nil
97+
}
98+
99+
func getTeamRole(roles []client.MemberRole) string {
100+
if slices.Contains(roles, client.MemberRoleOwner) {
101+
return "Owner"
102+
}
103+
if slices.Contains(roles, client.MemberRoleMember) {
104+
return "Member"
105+
}
106+
if len(roles) == 0 {
107+
return "None"
108+
}
109+
return "Unknown"
110+
}

pkg/commands/teams/list.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package teams
2+
3+
import (
4+
"encoding/json"
5+
"io"
6+
"sort"
7+
"strings"
8+
9+
"github.com/spf13/cobra"
10+
"gopkg.in/yaml.v3"
11+
12+
"github.com/intility/indev/internal/redact"
13+
"github.com/intility/indev/internal/telemetry"
14+
"github.com/intility/indev/internal/ux"
15+
"github.com/intility/indev/pkg/client"
16+
"github.com/intility/indev/pkg/clientset"
17+
"github.com/intility/indev/pkg/outputformat"
18+
)
19+
20+
func NewListCommand(set clientset.ClientSet) *cobra.Command {
21+
output := outputformat.Format("")
22+
cmd := &cobra.Command{
23+
Use: "list",
24+
Short: "List all teams",
25+
Long: `List all teams in the Intility Developer Platform`,
26+
PreRunE: set.EnsureSignedInPreHook,
27+
RunE: func(cmd *cobra.Command, args []string) error {
28+
ctx, span := telemetry.StartSpan(cmd.Context(), "teams.list")
29+
defer span.End()
30+
31+
cmd.SilenceUsage = true
32+
33+
teams, err := set.PlatformClient.ListTeams(ctx)
34+
if err != nil {
35+
return redact.Errorf("could not list teams: %w", redact.Safe(err))
36+
}
37+
38+
if len(teams) == 0 {
39+
ux.Fprint(cmd.OutOrStdout(), "No teams found\n")
40+
return nil
41+
}
42+
43+
if err = printTeamsList(cmd.OutOrStdout(), output, teams); err != nil {
44+
return redact.Errorf("could not print teams list: %w", redact.Safe(err))
45+
}
46+
47+
return nil
48+
},
49+
}
50+
51+
cmd.Flags().VarP(&output, "output", "o", "Output format (wide, json, yaml)")
52+
53+
return cmd
54+
}
55+
56+
func printTeamsList(writer io.Writer, format outputformat.Format, teams []client.Team) error {
57+
var err error
58+
59+
sort.Slice(teams, func(i, j int) bool {
60+
// list teams where authenticated user has membership first
61+
return strings.Join(teams[i].Role, ",") > strings.Join(teams[j].Role, ",")
62+
})
63+
64+
switch format {
65+
case "wide":
66+
table := ux.TableFromObjects(teams, func(team client.Team) []ux.Row {
67+
return []ux.Row{
68+
ux.NewRow("Id", team.ID),
69+
ux.NewRow("Name", team.Name),
70+
ux.NewRow("Description", team.Description),
71+
ux.NewRow("Role", strings.Join(team.Role, ",")),
72+
}
73+
})
74+
75+
ux.Fprint(writer, "%s", table.String())
76+
case "json":
77+
enc := json.NewEncoder(writer)
78+
enc.SetIndent("", " ")
79+
err = enc.Encode(teams)
80+
case "yaml":
81+
indent := 2
82+
enc := yaml.NewEncoder(writer)
83+
enc.SetIndent(indent)
84+
err = enc.Encode(teams)
85+
default:
86+
table := ux.TableFromObjects(teams, func(team client.Team) []ux.Row {
87+
return []ux.Row{
88+
ux.NewRow("Name", team.Name),
89+
ux.NewRow("Description", team.Description),
90+
ux.NewRow("Role", strings.Join(team.Role, ",")),
91+
}
92+
})
93+
94+
ux.Fprint(writer, "%s", table.String())
95+
}
96+
97+
if err != nil {
98+
return redact.Errorf("output encoder failed: %w", redact.Safe(err))
99+
}
100+
101+
return nil
102+
}

pkg/outputformat/outputformat.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package outputformat
2+
3+
import "errors"
4+
5+
var ErrInvalidOutputFormat = errors.New(`must be one of "wide", "json", "yaml"`)
6+
7+
type Format string
8+
9+
func (o *Format) String() string {
10+
return string(*o)
11+
}
12+
13+
func (o *Format) Set(value string) error {
14+
switch value {
15+
case "wide", "json", "yaml":
16+
*o = Format(value)
17+
return nil
18+
default:
19+
return ErrInvalidOutputFormat
20+
}
21+
}
22+
23+
func (o *Format) Type() string {
24+
return "outputFormat"
25+
}

0 commit comments

Comments
 (0)