Skip to content

Commit

Permalink
Improve cluster|project list-members subcommands (#399)
Browse files Browse the repository at this point in the history
  • Loading branch information
pmatseykanets authored Nov 1, 2024
1 parent 0267f06 commit 3d831ec
Show file tree
Hide file tree
Showing 12 changed files with 648 additions and 171 deletions.
70 changes: 49 additions & 21 deletions cmd/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"slices"
"strconv"
"strings"

"github.com/rancher/cli/cliclient"
"github.com/rancher/norman/types"
managementClient "github.com/rancher/rancher/pkg/client/generated/management/v3"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
Expand Down Expand Up @@ -182,14 +184,32 @@ func ClusterCommand() cli.Command {
Action: listClusterRoles,
},
{
Name: "list-members",
Usage: "List current members of the cluster",
Action: listClusterMembers,
Name: "list-members",
Usage: "List current members of the cluster",
Action: func(cctx *cli.Context) error {
client, err := GetClient(cctx)
if err != nil {
return err
}

return listClusterMembers(
cctx,
cctx.App.Writer,
client.UserConfig,
client.ManagementClient.ClusterRoleTemplateBinding,
client.ManagementClient.Principal,
)
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "cluster-id",
Usage: "Optional cluster ID to list members for, defaults to the current context",
},
cli.StringFlag{
Name: "format",
Usage: "'json', 'yaml' or Custom format: '{{.ID }} {{.Member }}'",
},
quietFlag,
},
},
},
Expand Down Expand Up @@ -587,49 +607,57 @@ func listClusterRoles(ctx *cli.Context) error {
return listRoles(ctx, "cluster")
}

func listClusterMembers(ctx *cli.Context) error {
c, err := GetClient(ctx)
if err != nil {
return err
}
type crtbLister interface {
List(opts *types.ListOpts) (*managementClient.ClusterRoleTemplateBindingCollection, error)
}

clusterID := c.UserConfig.FocusedCluster()
type userConfig interface {
FocusedCluster() string
FocusedProject() string
}

func listClusterMembers(ctx *cli.Context, out io.Writer, config userConfig, crtbs crtbLister, principals principalGetter) error {
clusterID := config.FocusedCluster()
if ctx.String("cluster-id") != "" {
clusterID = ctx.String("cluster-id")
}

filter := defaultListOpts(ctx)
filter.Filters["clusterId"] = clusterID
bindings, err := c.ManagementClient.ClusterRoleTemplateBinding.List(filter)
if err != nil {
return err
}

userFilter := defaultListOpts(ctx)
users, err := c.ManagementClient.User.List(userFilter)
bindings, err := crtbs.List(filter)
if err != nil {
return err
}

userMap := usersToNameMapping(users.Data)

var b []RoleTemplateBinding
rtbs := make([]RoleTemplateBinding, 0, len(bindings.Data))

for _, binding := range bindings.Data {
parsedTime, err := createdTimetoHuman(binding.Created)
if err != nil {
return err
}

b = append(b, RoleTemplateBinding{
principalID := binding.UserPrincipalID
if binding.GroupPrincipalID != "" {
principalID = binding.GroupPrincipalID
}

rtbs = append(rtbs, RoleTemplateBinding{
ID: binding.ID,
User: userMap[binding.UserID],
Member: getMemberNameFromPrincipal(principals, principalID),
Role: binding.RoleTemplateID,
Created: parsedTime,
})
}

return listRoleTemplateBindings(ctx, b)
writerConfig := &TableWriterConfig{
Format: ctx.String("format"),
Quiet: ctx.Bool("quiet"),
Writer: out,
}

return listRoleTemplateBindings(writerConfig, rtbs)
}

// getClusterRegToken will return an existing token or create one if none exist
Expand Down
99 changes: 99 additions & 0 deletions cmd/cluster_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package cmd

import (
"bytes"
"flag"
"fmt"
"net/url"
"testing"
"time"

"github.com/rancher/norman/types"
managementClient "github.com/rancher/rancher/pkg/client/generated/management/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli"
)

func TestListClusterMembers(t *testing.T) {
t.Parallel()

now := time.Now()

userConfig := &fakeUserConfig{
FocusedClusterFunc: func() string {
return "c-fn7lc"
},
}

created := now.Format(time.RFC3339)
crtbs := &fakeCRTBLister{
ListFunc: func(opts *types.ListOpts) (*managementClient.ClusterRoleTemplateBindingCollection, error) {
return &managementClient.ClusterRoleTemplateBindingCollection{
Data: []managementClient.ClusterRoleTemplateBinding{
{
Resource: types.Resource{
ID: "c-fn7lc:creator-cluster-owner",
},
Created: created,
RoleTemplateID: "cluster-owner",
UserPrincipalID: "local://user-2p7w6",
},
{
Resource: types.Resource{
ID: "c-fn7lc:crtb-qd49d",
},
Created: created,
RoleTemplateID: "cluster-member",
GroupPrincipalID: "okta_group://b4qkhsnliz",
},
},
}, nil
},
}

principals := &fakePrincipalGetter{
ByIDFunc: func(id string) (*managementClient.Principal, error) {
id, err := url.PathUnescape(id)
require.NoError(t, err)

switch id {
case "local://user-2p7w6":
return &managementClient.Principal{
Name: "Default Admin",
LoginName: "admin",
Provider: "local",
PrincipalType: "user",
}, nil
case "okta_group://b4qkhsnliz":
return &managementClient.Principal{
Name: "DevOps",
LoginName: "devops",
Provider: "okta",
PrincipalType: "group",
}, nil
default:
return nil, fmt.Errorf("not found")
}
},
}

flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
cctx := cli.NewContext(nil, flagSet, nil)

var out bytes.Buffer

err := listClusterMembers(cctx, &out, userConfig, crtbs, principals)
require.NoError(t, err)
require.NotEmpty(t, out)

humanCreated := now.Format(humanTimeFormat)
want := [][]string{
{"BINDING-ID", "MEMBER", "ROLE", "CREATED"},
{"c-fn7lc:creator-cluster-owner", "Default Admin (Local User)", "cluster-owner", humanCreated},
{"c-fn7lc:crtb-qd49d", "DevOps (Okta Group)", "cluster-member", humanCreated},
}

got := parseTabWriterOutput(&out)
assert.Equal(t, want, got)
}
78 changes: 78 additions & 0 deletions cmd/cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package cmd

import (
"bufio"
"io"
"strings"

"github.com/rancher/norman/types"
managementClient "github.com/rancher/rancher/pkg/client/generated/management/v3"
)

type fakePrincipalGetter struct {
ByIDFunc func(id string) (*managementClient.Principal, error)
}

func (g *fakePrincipalGetter) ByID(id string) (*managementClient.Principal, error) {
if g.ByIDFunc != nil {
return g.ByIDFunc(id)
}
return nil, nil
}

type fakeUserConfig struct {
FocusedClusterFunc func() string
FocusedProjectFunc func() string
}

func (c *fakeUserConfig) FocusedCluster() string {
if c.FocusedClusterFunc != nil {
return c.FocusedClusterFunc()
}
return ""
}

func (c *fakeUserConfig) FocusedProject() string {
if c.FocusedProjectFunc != nil {
return c.FocusedProjectFunc()
}
return ""
}

type fakeCRTBLister struct {
ListFunc func(opts *types.ListOpts) (*managementClient.ClusterRoleTemplateBindingCollection, error)
}

func (f *fakeCRTBLister) List(opts *types.ListOpts) (*managementClient.ClusterRoleTemplateBindingCollection, error) {
if f.ListFunc != nil {
return f.ListFunc(opts)
}
return nil, nil
}

type fakePRTBLister struct {
ListFunc func(opts *types.ListOpts) (*managementClient.ProjectRoleTemplateBindingCollection, error)
}

func (f *fakePRTBLister) List(opts *types.ListOpts) (*managementClient.ProjectRoleTemplateBindingCollection, error) {
if f.ListFunc != nil {
return f.ListFunc(opts)
}
return nil, nil
}

func parseTabWriterOutput(r io.Reader) [][]string {
var parsed [][]string
scanner := bufio.NewScanner(r)
for scanner.Scan() {
var fields []string
for _, field := range strings.Split(scanner.Text(), " ") {
if field == "" {
continue
}
fields = append(fields, strings.TrimSpace(field))
}
parsed = append(parsed, fields)
}
return parsed
}
Loading

0 comments on commit 3d831ec

Please sign in to comment.