From 3d831ec6cffead0702fb3c7f7059ec903aab9ec2 Mon Sep 17 00:00:00 2001 From: Peter Matseykanets Date: Fri, 1 Nov 2024 14:46:29 -0400 Subject: [PATCH] Improve cluster|project list-members subcommands (#399) Ref: https://github.com/rancher/rancher/issues/47692 --- cmd/cluster.go | 70 +++++++++----- cmd/cluster_test.go | 99 ++++++++++++++++++++ cmd/cmd_test.go | 78 ++++++++++++++++ cmd/common.go | 76 +++++++++------ cmd/common_test.go | 213 ++++++++++++++++++++++++++++++++++-------- cmd/project.go | 65 ++++++++----- cmd/project_test.go | 99 ++++++++++++++++++++ config/config.go | 10 +- config/config_test.go | 6 +- go.mod | 4 - go.sum | 2 - main_test.go | 97 +++++++++---------- 12 files changed, 648 insertions(+), 171 deletions(-) create mode 100644 cmd/cluster_test.go create mode 100644 cmd/cmd_test.go create mode 100644 cmd/project_test.go diff --git a/cmd/cluster.go b/cmd/cluster.go index 9e5a30f6a..966ba112b 100644 --- a/cmd/cluster.go +++ b/cmd/cluster.go @@ -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" @@ -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, }, }, }, @@ -587,33 +607,30 @@ 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) @@ -621,15 +638,26 @@ func listClusterMembers(ctx *cli.Context) error { 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 diff --git a/cmd/cluster_test.go b/cmd/cluster_test.go new file mode 100644 index 000000000..e4dc4a5ce --- /dev/null +++ b/cmd/cluster_test.go @@ -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) +} diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go new file mode 100644 index 000000000..7b19a97dc --- /dev/null +++ b/cmd/cmd_test.go @@ -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 +} diff --git a/cmd/common.go b/cmd/common.go index 3fe36eae5..18062b072 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -74,7 +74,7 @@ type RoleTemplate struct { type RoleTemplateBinding struct { ID string - User string + Member string Role string Created string } @@ -121,27 +121,60 @@ func listRoles(ctx *cli.Context, context string) error { return writer.Err() } -func listRoleTemplateBindings(ctx *cli.Context, b []RoleTemplateBinding) error { - writer := NewTableWriter([][]string{ +func listRoleTemplateBindings(writerConfig *TableWriterConfig, rtbs []RoleTemplateBinding) error { + writer := NewTableWriterWithConfig([][]string{ {"BINDING-ID", "ID"}, - {"USER", "User"}, + {"MEMBER", "Member"}, {"ROLE", "Role"}, {"CREATED", "Created"}, - }, ctx) - + }, writerConfig) defer writer.Close() - for _, item := range b { - writer.Write(&RoleTemplateBinding{ - ID: item.ID, - User: item.User, - Role: item.Role, - Created: item.Created, - }) + for _, rtb := range rtbs { + writer.Write(&rtb) } + return writer.Err() } +type principalGetter interface { + ByID(id string) (*managementClient.Principal, error) +} + +func getMemberNameFromPrincipal(principals principalGetter, principalID string) string { + principal, err := principals.ByID(url.PathEscape(principalID)) + if err != nil { + principal = parsePrincipalID(principalID) + } + + return fmt.Sprintf( + "%s (%s %s)", + principal.Name, + cases.Title(language.Und).String(principal.Provider), + cases.Title(language.Und).String(principal.PrincipalType), + ) +} + +func parsePrincipalID(principalID string) *managementClient.Principal { + scheme, id, _ := strings.Cut(principalID, "://") + provider, ptype, _ := strings.Cut(scheme, "_") + + if provider == "local" && ptype == "" { + ptype = "user" + } + + if ptype != "user" { + ptype = "group" + } + + return &managementClient.Principal{ + Name: id, + LoginName: id, + Provider: provider, + PrincipalType: ptype, + } +} + func getKubeConfigForUser(ctx *cli.Context, user string) (*api.Config, error) { cf, err := loadConfig(ctx) if err != nil { @@ -176,19 +209,6 @@ func setKubeConfigForUser(ctx *cli.Context, user string, kubeConfig *api.Config) return cf.Write() } -func usersToNameMapping(u []managementClient.User) map[string]string { - userMapping := make(map[string]string) - for _, user := range u { - if user.Name != "" { - userMapping[user.ID] = user.Name - - } else { - userMapping[user.ID] = user.Username - } - } - return userMapping -} - func searchForMember(ctx *cli.Context, c *cliclient.MasterClient, name string) (*managementClient.Principal, error) { filter := defaultListOpts(ctx) filter.Filters["ID"] = "thisisnotathingIhope" @@ -595,12 +615,14 @@ func getClusterName(cluster *managementClient.Cluster) string { return cluster.ID } +const humanTimeFormat = "02 Jan 2006 15:04:05 MST" + func createdTimetoHuman(t string) (string, error) { parsedTime, err := time.Parse(time.RFC3339, t) if err != nil { return "", err } - return parsedTime.Format("02 Jan 2006 15:04:05 MST"), nil + return parsedTime.Format(humanTimeFormat), nil } func outputMembers(ctx *cli.Context, c *cliclient.MasterClient, members []managementClient.Member) error { diff --git a/cmd/common_test.go b/cmd/common_test.go index 39d148dc4..c50cf0132 100644 --- a/cmd/common_test.go +++ b/cmd/common_test.go @@ -1,71 +1,202 @@ package cmd import ( + "fmt" + "net/url" + "strconv" "testing" - "gopkg.in/check.v1" + managementClient "github.com/rancher/rancher/pkg/client/generated/management/v3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Hook up gocheck into the "go test" runner. -func Test(t *testing.T) { - check.TestingT(t) -} +func TestParseClusterAndProjectID(t *testing.T) { + t.Parallel() -type CommonTestSuite struct { -} + tests := []struct { + id, + cluster string + project string + shouldErr bool + }{ + { + id: "local:p-12345", + cluster: "local", + project: "p-12345", + }, + { + id: "c-12345:p-12345", + cluster: "c-12345", + project: "p-12345", + }, + { + id: "cocal:p-12345", + shouldErr: true, + }, + { + id: "c-123:p-123", + shouldErr: true, + }, + { + shouldErr: true, + }, + { + id: "c-m-12345678:p-12345", + cluster: "c-m-12345678", + project: "p-12345", + }, + { + id: "c-m-123:p-12345", + shouldErr: true, + }, + } -var _ = check.Suite(&CommonTestSuite{}) + for _, test := range tests { + test := test + t.Run(test.id, func(t *testing.T) { + t.Parallel() -func (s *CommonTestSuite) SetUpSuite(c *check.C) { + cluster, project, err := parseClusterAndProjectID(test.id) + if test.shouldErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.cluster, cluster) + assert.Equal(t, test.project, project) + }) + } } -func (s *CommonTestSuite) TestParseClusterAndProjectID(c *check.C) { - testParse(c, "local:p-12345", "local", "p-12345", false) - testParse(c, "c-12345:p-12345", "c-12345", "p-12345", false) - testParse(c, "cocal:p-12345", "", "", true) - testParse(c, "c-123:p-123", "", "", true) - testParse(c, "", "", "", true) - testParse(c, "c-m-12345678:p-12345", "c-m-12345678", "p-12345", false) - testParse(c, "c-m-123:p-12345", "", "", true) -} +func TestConvertSnakeCaseKeysToCamelCase(t *testing.T) { + t.Parallel() -func (s *CommonTestSuite) TestConvertSnakeCaseKeysToCamelCase(c *check.C) { - cases := []struct { - input map[string]interface{} - renamed map[string]interface{} + tests := []struct { + input map[string]any + want map[string]any }{ { - map[string]interface{}{"foo_bar": "hello"}, - map[string]interface{}{"fooBar": "hello"}, + map[string]any{"foo_bar": "hello"}, + map[string]any{"fooBar": "hello"}, + }, + { + map[string]any{"fooBar": "hello"}, + map[string]any{"fooBar": "hello"}, + }, + { + map[string]any{"foobar": "hello", "some_key": "valueUnmodified", "bar-baz": "bar-baz"}, + map[string]any{"foobar": "hello", "someKey": "valueUnmodified", "bar-baz": "bar-baz"}, }, { - map[string]interface{}{"fooBar": "hello"}, - map[string]interface{}{"fooBar": "hello"}, + map[string]any{"foo_bar": "hello", "backup_config": map[string]any{"hello_world": true}, "config_id": 123}, + map[string]any{"fooBar": "hello", "backupConfig": map[string]any{"helloWorld": true}, "configId": 123}, }, + } + + for i, test := range tests { + test := test + t.Run(strconv.Itoa(i), func(t *testing.T) { + t.Parallel() + + convertSnakeCaseKeysToCamelCase(test.input) + assert.Equal(t, test.input, test.want) + }) + } +} + +func TestParsePrincipalID(t *testing.T) { + t.Parallel() + + tests := []struct { + id string + want *managementClient.Principal + }{ { - map[string]interface{}{"foobar": "hello", "some_key": "valueUnmodified", "bar-baz": "bar-baz"}, - map[string]interface{}{"foobar": "hello", "someKey": "valueUnmodified", "bar-baz": "bar-baz"}, + id: "local://user-2p7w6", + want: &managementClient.Principal{ + Name: "user-2p7w6", + LoginName: "user-2p7w6", + Provider: "local", + PrincipalType: "user", + }, }, { - map[string]interface{}{"foo_bar": "hello", "backup_config": map[string]interface{}{"hello_world": true}, "config_id": 123}, - map[string]interface{}{"fooBar": "hello", "backupConfig": map[string]interface{}{"helloWorld": true}, "configId": 123}, + id: "okta_group://b4qkhsnliz", + want: &managementClient.Principal{ + Name: "b4qkhsnliz", + LoginName: "b4qkhsnliz", + Provider: "okta", + PrincipalType: "group", + }, }, } - for _, tc := range cases { - convertSnakeCaseKeysToCamelCase(tc.input) - c.Assert(tc.input, check.DeepEquals, tc.renamed) + for _, test := range tests { + test := test + t.Run(test.id, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, test.want, parsePrincipalID(test.id)) + }) } } -func testParse(c *check.C, testID, expectedCluster, expectedProject string, errorExpected bool) { - actualCluster, actualProject, actualErr := parseClusterAndProjectID(testID) - c.Assert(actualCluster, check.Equals, expectedCluster) - c.Assert(actualProject, check.Equals, expectedProject) - if errorExpected { - c.Assert(actualErr, check.NotNil) - } else { - c.Assert(actualErr, check.IsNil) +func TestGetMemberNameFromPrincipal(t *testing.T) { + t.Parallel() + + 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") + } + }, + } + + tests := []struct { + id string + want string + }{ + { + id: "local://user-2p7w6", + want: "Default Admin (Local User)", + }, + { + id: "okta_group://b4qkhsnliz", + want: "DevOps (Okta Group)", + }, + { + id: "okta_user://lfql6h5tmh", + want: "lfql6h5tmh (Okta User)", + }, + } + + for _, test := range tests { + test := test + t.Run(test.id, func(t *testing.T) { + t.Parallel() + + got := getMemberNameFromPrincipal(principals, test.id) + assert.Equal(t, test.want, got) + }) } } diff --git a/cmd/project.go b/cmd/project.go index fe9658934..18d0b2ace 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -2,8 +2,10 @@ package cmd import ( "fmt" + "io" "github.com/rancher/cli/cliclient" + "github.com/rancher/norman/types" managementClient "github.com/rancher/rancher/pkg/client/generated/management/v3" "github.com/urfave/cli" ) @@ -90,14 +92,32 @@ func ProjectCommand() cli.Command { Action: listProjectRoles, }, { - Name: "list-members", - Usage: "List current members of the project", - Action: listProjectMembers, + Name: "list-members", + Usage: "List current members of the project", + Action: func(cctx *cli.Context) error { + client, err := GetClient(cctx) + if err != nil { + return err + } + + return listProjectMembers( + cctx, + cctx.App.Writer, + client.UserConfig, + client.ManagementClient.ProjectRoleTemplateBinding, + client.ManagementClient.Principal, + ) + }, Flags: []cli.Flag{ cli.StringFlag{ Name: "project-id", Usage: "Optional project ID to list members for, defaults to the current context", }, + cli.StringFlag{ + Name: "format", + Usage: "'json', 'yaml' or Custom format: '{{.ID }} {{.Member }}'", + }, + quietFlag, }, }, }, @@ -292,33 +312,25 @@ func listProjectRoles(ctx *cli.Context) error { return listRoles(ctx, "project") } -func listProjectMembers(ctx *cli.Context) error { - c, err := GetClient(ctx) - if err != nil { - return err - } +type prtbLister interface { + List(opts *types.ListOpts) (*managementClient.ProjectRoleTemplateBindingCollection, error) +} - projectID := c.UserConfig.Project +func listProjectMembers(ctx *cli.Context, out io.Writer, config userConfig, prtbs prtbLister, principals principalGetter) error { + projectID := config.FocusedProject() if ctx.String("project-id") != "" { projectID = ctx.String("project-id") } filter := defaultListOpts(ctx) filter.Filters["projectId"] = projectID - bindings, err := c.ManagementClient.ProjectRoleTemplateBinding.List(filter) - if err != nil { - return err - } - userFilter := defaultListOpts(ctx) - users, err := c.ManagementClient.User.List(userFilter) + bindings, err := prtbs.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) @@ -326,15 +338,26 @@ func listProjectMembers(ctx *cli.Context) error { 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) } func getProjectList( diff --git a/cmd/project_test.go b/cmd/project_test.go new file mode 100644 index 000000000..422fe3482 --- /dev/null +++ b/cmd/project_test.go @@ -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 TestListProjectMembers(t *testing.T) { + t.Parallel() + + now := time.Now() + + userConfig := &fakeUserConfig{ + FocusedProjectFunc: func() string { + return "c-fn7lc:p-9mdxl" + }, + } + + created := now.Format(time.RFC3339) + prtbs := &fakePRTBLister{ + ListFunc: func(opts *types.ListOpts) (*managementClient.ProjectRoleTemplateBindingCollection, error) { + return &managementClient.ProjectRoleTemplateBindingCollection{ + Data: []managementClient.ProjectRoleTemplateBinding{ + { + Resource: types.Resource{ + ID: "p-9mdxl:creator-project-owner", + }, + Created: created, + RoleTemplateID: "project-owner", + UserPrincipalID: "local://user-2p7w6", + }, + { + Resource: types.Resource{ + ID: "p-9mdxl:prtb-mqcvk", + }, + Created: created, + RoleTemplateID: "project-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 := listProjectMembers(cctx, &out, userConfig, prtbs, principals) + require.NoError(t, err) + require.NotEmpty(t, out) + + humanCreated := now.Format(humanTimeFormat) + want := [][]string{ + {"BINDING-ID", "MEMBER", "ROLE", "CREATED"}, + {"p-9mdxl:creator-project-owner", "Default Admin (Local User)", "project-owner", humanCreated}, + {"p-9mdxl:prtb-mqcvk", "DevOps (Okta Group)", "project-member", humanCreated}, + } + + got := parseTabWriterOutput(&out) + assert.Equal(t, want, got) +} diff --git a/config/config.go b/config/config.go index c6200d7c1..60a873f01 100644 --- a/config/config.go +++ b/config/config.go @@ -112,7 +112,15 @@ func (c Config) FocusedServer() (*ServerConfig, error) { } func (c ServerConfig) FocusedCluster() string { - return strings.Split(c.Project, ":")[0] + cluster, _, ok := strings.Cut(c.Project, ":") + if !ok { + return "" + } + return cluster +} + +func (c ServerConfig) FocusedProject() string { + return c.Project } func (c ServerConfig) KubeToken(key string) *ExecCredential { diff --git a/config/config_test.go b/config/config_test.go index 5d2c44c36..c3b9da2cc 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -28,7 +28,7 @@ const ( invalidFile = `invalid config file` ) -func Test_GetFilePermissionWarnings(t *testing.T) { +func TestGetFilePermissionWarnings(t *testing.T) { t.Parallel() tests := []struct { @@ -78,7 +78,7 @@ func Test_GetFilePermissionWarnings(t *testing.T) { } } -func Test_Permission(t *testing.T) { +func TestPermission(t *testing.T) { t.Parallel() // New config files should have 0600 permissions @@ -131,7 +131,7 @@ func Test_Permission(t *testing.T) { }) } -func Test_LoadFromPath(t *testing.T) { +func TestLoadFromPath(t *testing.T) { t.Parallel() tests := []struct { diff --git a/go.mod b/go.mod index bbefe2647..04e9f08fd 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,6 @@ require ( golang.org/x/sync v0.7.0 golang.org/x/term v0.22.0 golang.org/x/text v0.16.0 - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v2 v2.4.0 k8s.io/client-go v12.0.0+incompatible ) @@ -49,8 +48,6 @@ require ( github.com/imdario/mergo v0.3.16 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kr/pretty v0.3.1 // indirect - github.com/kr/text v0.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -67,7 +64,6 @@ require ( github.com/rancher/lasso v0.0.0-20240705194423-b2a060d103c1 // indirect github.com/rancher/rke v1.6.0-rc9 // indirect github.com/rancher/wrangler/v3 v3.0.0 // indirect - github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tidwall/match v1.1.1 // indirect diff --git a/go.sum b/go.sum index 57a4f9f6a..a58e57688 100644 --- a/go.sum +++ b/go.sum @@ -82,7 +82,6 @@ github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -115,7 +114,6 @@ github.com/rancher/rke v1.6.0-rc9 h1:P8EVV4hl3cG8CHDuFUEfqFK8sC92aLoemFzmeFQOCck github.com/rancher/rke v1.6.0-rc9/go.mod h1:5xRbf3L8PxqJRhABjYRfaBqbpVqAnqyH3maUNQEuwvk= github.com/rancher/wrangler/v3 v3.0.0 h1:IHHCA+vrghJDPxjtLk4fmeSCFhNe9fFzLFj3m2B0YpA= github.com/rancher/wrangler/v3 v3.0.0/go.mod h1:Dfckuuq7MJk2JWVBDywRlZXMxEyPxHy4XqGrPEzu5Eg= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/main_test.go b/main_test.go index 5bf6730ae..ff858619a 100644 --- a/main_test.go +++ b/main_test.go @@ -3,62 +3,57 @@ package main import ( "testing" - "gopkg.in/check.v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// Hook up gocheck into the "go test" runner. -func Test(t *testing.T) { - check.TestingT(t) -} - -type MainTestSuite struct { -} - -var _ = check.Suite(&MainTestSuite{}) - -func (m *MainTestSuite) SetUpSuite(c *check.C) { -} - -func (m *MainTestSuite) TestParseArgs(c *check.C) { - input := [][]string{ - {"rancher", "run", "--debug", "-itd"}, - {"rancher", "run", "--debug", "-itf=b"}, - {"rancher", "run", "--debug", "-itd#"}, - {"rancher", "run", "--debug", "-f=b"}, - {"rancher", "run", "--debug", "-=b"}, - {"rancher", "run", "--debug", "-"}, +func TestParseArgs(t *testing.T) { + t.Parallel() + + tests := []struct { + input string + want []string + shouldErr bool + }{ + { + input: "-itd", + want: []string{"-i", "-t", "-d"}, + }, + { + input: "-itf=b", + want: []string{"-i", "-t", "-f=b"}, + }, + { + input: "-itd#", + shouldErr: true, + }, + { + input: "-f=b", + want: []string{"-f=b"}, + }, + { + input: "-=b", + shouldErr: true, + }, + { + input: "-", + want: []string{"-"}, + }, } - r0, err := parseArgs(input[0]) - if err != nil { - c.Fatal(err) - } - c.Assert(r0, check.DeepEquals, []string{"rancher", "run", "--debug", "-i", "-t", "-d"}) - - r1, err := parseArgs(input[1]) - if err != nil { - c.Fatal(err) - } - c.Assert(r1, check.DeepEquals, []string{"rancher", "run", "--debug", "-i", "-t", "-f=b"}) - _, err = parseArgs(input[2]) - if err == nil { - c.Fatal("should raise error") - } + for _, test := range tests { + test := test + t.Run(test.input, func(t *testing.T) { + t.Parallel() - r3, err := parseArgs(input[3]) - if err != nil { - c.Fatal(err) - } - c.Assert(r3, check.DeepEquals, []string{"rancher", "run", "--debug", "-f=b"}) - - _, err = parseArgs(input[4]) - if err == nil { - c.Fatal("should raise error") - } + got, err := parseArgs([]string{"rancher", "run", "--debug", test.input}) + if test.shouldErr { + require.Error(t, err) + return + } - r5, err := parseArgs(input[5]) - if err != nil { - c.Fatal(err) + require.NoError(t, err) + assert.Equal(t, append([]string{"rancher", "run", "--debug"}, test.want...), got) + }) } - c.Assert(r5, check.DeepEquals, []string{"rancher", "run", "--debug", "-"}) }