From 95b189fd8f69812738ba9ea472237ab946863d23 Mon Sep 17 00:00:00 2001 From: Eddie Zaneski Date: Fri, 3 Apr 2020 16:41:46 -0600 Subject: [PATCH] Add get-users and delete-user to kubectl config Signed-off-by: Eddie Zaneski --- build/visible_to/BUILD | 1 + .../src/k8s.io/kubectl/pkg/cmd/config/BUILD | 9 +- .../k8s.io/kubectl/pkg/cmd/config/config.go | 2 + .../kubectl/pkg/cmd/config/delete_user.go | 120 +++++++++++ .../pkg/cmd/config/delete_user_test.go | 202 ++++++++++++++++++ .../kubectl/pkg/cmd/config/get_users.go | 90 ++++++++ .../kubectl/pkg/cmd/config/get_users_test.go | 75 +++++++ .../k8s.io/kubectl/pkg/cmd/testing/fake.go | 19 ++ 8 files changed, 515 insertions(+), 3 deletions(-) create mode 100644 staging/src/k8s.io/kubectl/pkg/cmd/config/delete_user.go create mode 100644 staging/src/k8s.io/kubectl/pkg/cmd/config/delete_user_test.go create mode 100644 staging/src/k8s.io/kubectl/pkg/cmd/config/get_users.go create mode 100644 staging/src/k8s.io/kubectl/pkg/cmd/config/get_users_test.go diff --git a/build/visible_to/BUILD b/build/visible_to/BUILD index efa83c4fe5a..2398b9ffb02 100644 --- a/build/visible_to/BUILD +++ b/build/visible_to/BUILD @@ -245,6 +245,7 @@ package_group( "//staging/src/k8s.io/kubectl/pkg/cmd/attach", "//staging/src/k8s.io/kubectl/pkg/cmd/certificates", "//staging/src/k8s.io/kubectl/pkg/cmd/clusterinfo", + "//staging/src/k8s.io/kubectl/pkg/cmd/config", "//staging/src/k8s.io/kubectl/pkg/cmd/cp", "//staging/src/k8s.io/kubectl/pkg/cmd/create", "//staging/src/k8s.io/kubectl/pkg/cmd/delete", diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/config/BUILD b/staging/src/k8s.io/kubectl/pkg/cmd/config/BUILD index 3f739becaf6..5481c53d919 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/config/BUILD +++ b/staging/src/k8s.io/kubectl/pkg/cmd/config/BUILD @@ -14,8 +14,10 @@ go_library( "current_context.go", "delete_cluster.go", "delete_context.go", + "delete_user.go", "get_clusters.go", "get_contexts.go", + "get_users.go", "navigation_step_parser.go", "rename_context.go", "set.go", @@ -25,9 +27,7 @@ go_library( ], importmap = "k8s.io/kubernetes/vendor/k8s.io/kubectl/pkg/cmd/config", importpath = "k8s.io/kubectl/pkg/cmd/config", - visibility = [ - "//build/visible_to:pkg_kubectl_cmd_config_CONSUMERS", - ], + visibility = ["//visibility:public"], deps = [ "//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", @@ -56,8 +56,10 @@ go_test( "current_context_test.go", "delete_cluster_test.go", "delete_context_test.go", + "delete_user_test.go", "get_clusters_test.go", "get_contexts_test.go", + "get_users_test.go", "navigation_step_parser_test.go", "rename_context_test.go", "set_test.go", @@ -73,6 +75,7 @@ go_test( "//staging/src/k8s.io/client-go/tools/clientcmd:go_default_library", "//staging/src/k8s.io/client-go/tools/clientcmd/api:go_default_library", "//staging/src/k8s.io/component-base/cli/flag:go_default_library", + "//staging/src/k8s.io/kubectl/pkg/cmd/testing:go_default_library", "//staging/src/k8s.io/kubectl/pkg/cmd/util:go_default_library", ], ) diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/config/config.go b/staging/src/k8s.io/kubectl/pkg/cmd/config/config.go index 8ec6529440d..7848c396145 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/config/config.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/config/config.go @@ -65,8 +65,10 @@ func NewCmdConfig(f cmdutil.Factory, pathOptions *clientcmd.PathOptions, streams cmd.AddCommand(NewCmdConfigUseContext(streams.Out, pathOptions)) cmd.AddCommand(NewCmdConfigGetContexts(streams, pathOptions)) cmd.AddCommand(NewCmdConfigGetClusters(streams.Out, pathOptions)) + cmd.AddCommand(NewCmdConfigGetUsers(streams, pathOptions)) cmd.AddCommand(NewCmdConfigDeleteCluster(streams.Out, pathOptions)) cmd.AddCommand(NewCmdConfigDeleteContext(streams.Out, streams.ErrOut, pathOptions)) + cmd.AddCommand(NewCmdConfigDeleteUser(streams, pathOptions)) cmd.AddCommand(NewCmdConfigRenameContext(streams.Out, pathOptions)) return cmd diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/config/delete_user.go b/staging/src/k8s.io/kubectl/pkg/cmd/config/delete_user.go new file mode 100644 index 00000000000..66f709bcab9 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/config/delete_user.go @@ -0,0 +1,120 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import ( + "fmt" + + "github.com/spf13/cobra" + + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/i18n" + "k8s.io/kubectl/pkg/util/templates" +) + +var ( + deleteUserExample = templates.Examples(` + # Delete the minikube user + kubectl config delete-user minikube`) +) + +// DeleteUserOptions holds the data needed to run the command +type DeleteUserOptions struct { + user string + + configAccess clientcmd.ConfigAccess + config *clientcmdapi.Config + configFile string + + genericclioptions.IOStreams +} + +// NewDeleteUserOptions creates the options for the command +func NewDeleteUserOptions(ioStreams genericclioptions.IOStreams, configAccess clientcmd.ConfigAccess) *DeleteUserOptions { + return &DeleteUserOptions{ + configAccess: configAccess, + IOStreams: ioStreams, + } +} + +// NewCmdConfigDeleteUser returns a Command instance for 'config delete-user' sub command +func NewCmdConfigDeleteUser(streams genericclioptions.IOStreams, configAccess clientcmd.ConfigAccess) *cobra.Command { + o := NewDeleteUserOptions(streams, configAccess) + + cmd := &cobra.Command{ + Use: "delete-user NAME", + DisableFlagsInUseLine: true, + Short: i18n.T("Delete the specified user from the kubeconfig"), + Long: "Delete the specified user from the kubeconfig", + Example: deleteUserExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Complete(cmd, args)) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Run()) + }, + } + + return cmd +} + +// Complete sets up the command to run +func (o *DeleteUserOptions) Complete(cmd *cobra.Command, args []string) error { + config, err := o.configAccess.GetStartingConfig() + if err != nil { + return err + } + o.config = config + + if len(args) != 1 { + return cmdutil.UsageErrorf(cmd, "user to delete is required") + } + o.user = args[0] + + configFile := o.configAccess.GetDefaultFilename() + if o.configAccess.IsExplicitFile() { + configFile = o.configAccess.GetExplicitFile() + } + o.configFile = configFile + + return nil +} + +// Validate ensures the command has enough info to run +func (o *DeleteUserOptions) Validate() error { + _, ok := o.config.AuthInfos[o.user] + if !ok { + return fmt.Errorf("cannot delete user %s, not in %s", o.user, o.configFile) + } + + return nil +} + +// Run performs the command +func (o *DeleteUserOptions) Run() error { + delete(o.config.AuthInfos, o.user) + + if err := clientcmd.ModifyConfig(o.configAccess, *o.config, true); err != nil { + return err + } + + fmt.Fprintf(o.Out, "deleted user %s from %s\n", o.user, o.configFile) + + return nil +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/config/delete_user_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/config/delete_user_test.go new file mode 100644 index 00000000000..94a4f4d705f --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/config/delete_user_test.go @@ -0,0 +1,202 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import ( + "reflect" + "strings" + "testing" + + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" +) + +func TestDeleteUserComplete(t *testing.T) { + var tests = []struct { + name string + args []string + err string + }{ + { + name: "no args", + args: []string{}, + err: "user to delete is required", + }, + { + name: "user provided", + args: []string{"minikube"}, + err: "", + }, + } + + for i := range tests { + test := tests[i] + t.Run(test.name, func(t *testing.T) { + tf := cmdtesting.NewTestFactory() + defer tf.Cleanup() + + ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() + pathOptions, err := tf.PathOptionsWithConfig(clientcmdapi.Config{}) + if err != nil { + t.Fatalf("unexpected error executing command: %v", err) + } + + cmd := NewCmdConfigDeleteUser(ioStreams, pathOptions) + cmd.SetOut(out) + options := NewDeleteUserOptions(ioStreams, pathOptions) + + if err := options.Complete(cmd, test.args); err != nil { + if test.err == "" { + t.Fatalf("unexpected error executing command: %v", err) + } + + if !strings.Contains(err.Error(), test.err) { + t.Fatalf("expected error to contain %v, got %v", test.err, err.Error()) + } + + return + } + + if options.configFile != pathOptions.GlobalFile { + t.Fatalf("expected configFile to be %v, got %v", pathOptions.GlobalFile, options.configFile) + } + }) + } +} + +func TestDeleteUserValidate(t *testing.T) { + var tests = []struct { + name string + user string + config clientcmdapi.Config + err string + }{ + { + name: "user not in config", + user: "kube", + config: clientcmdapi.Config{ + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "minikube": {Username: "minikube"}, + }, + }, + err: "cannot delete user kube", + }, + { + name: "user in config", + user: "kube", + config: clientcmdapi.Config{ + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "minikube": {Username: "minikube"}, + "kube": {Username: "kube"}, + }, + }, + err: "", + }, + } + + for i := range tests { + test := tests[i] + t.Run(test.name, func(t *testing.T) { + tf := cmdtesting.NewTestFactory() + defer tf.Cleanup() + + ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() + pathOptions, err := tf.PathOptionsWithConfig(test.config) + if err != nil { + t.Fatalf("unexpected error executing command: %v", err) + } + + options := NewDeleteUserOptions(ioStreams, pathOptions) + options.config = &test.config + options.user = test.user + + if err := options.Validate(); err != nil { + if !strings.Contains(err.Error(), test.err) { + t.Fatalf("expected: %s but got %s", test.err, err.Error()) + } + + return + } + }) + } +} + +func TestDeleteUserRun(t *testing.T) { + var tests = []struct { + name string + user string + config clientcmdapi.Config + expectedUsers []string + out string + }{ + { + name: "delete user", + user: "kube", + config: clientcmdapi.Config{ + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "minikube": {Username: "minikube"}, + "kube": {Username: "kube"}, + }, + }, + expectedUsers: []string{"minikube"}, + out: "deleted user kube from", + }, + } + + for i := range tests { + test := tests[i] + t.Run(test.name, func(t *testing.T) { + tf := cmdtesting.NewTestFactory() + defer tf.Cleanup() + + ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() + pathOptions, err := tf.PathOptionsWithConfig(test.config) + if err != nil { + t.Fatalf("unexpected error executing command: %v", err) + } + + options := NewDeleteUserOptions(ioStreams, pathOptions) + options.config = &test.config + options.configFile = pathOptions.GlobalFile + options.user = test.user + + if err := options.Run(); err != nil { + t.Fatalf("unexpected error executing command: %v", err) + } + + if got := out.String(); !strings.Contains(got, test.out) { + t.Fatalf("expected: %s but got %s", test.out, got) + } + + config, err := clientcmd.LoadFromFile(options.configFile) + if err != nil { + t.Fatalf("unexpected error executing command: %v", err) + } + + users := make([]string, 0, len(config.AuthInfos)) + for user := range config.AuthInfos { + users = append(users, user) + } + + if !reflect.DeepEqual(test.expectedUsers, users) { + t.Fatalf("expected %v, got %v", test.expectedUsers, users) + } + }) + } +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/config/get_users.go b/staging/src/k8s.io/kubectl/pkg/cmd/config/get_users.go new file mode 100644 index 00000000000..6feb4abfb84 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/config/get_users.go @@ -0,0 +1,90 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import ( + "fmt" + "sort" + + "github.com/spf13/cobra" + + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/tools/clientcmd" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/i18n" + "k8s.io/kubectl/pkg/util/templates" +) + +var ( + getUsersExample = templates.Examples(` + # List the users kubectl knows about + kubectl config get-users`) +) + +// GetUsersOptions holds the data needed to run the command +type GetUsersOptions struct { + configAccess clientcmd.ConfigAccess + + genericclioptions.IOStreams +} + +// NewGetUsersOptions creates the options for the command +func NewGetUsersOptions(ioStreams genericclioptions.IOStreams, configAccess clientcmd.ConfigAccess) *GetUsersOptions { + return &GetUsersOptions{ + configAccess: configAccess, + IOStreams: ioStreams, + } +} + +// NewCmdConfigGetUsers creates a command object for the "get-users" action, which +// lists all users defined in the kubeconfig. +func NewCmdConfigGetUsers(streams genericclioptions.IOStreams, configAccess clientcmd.ConfigAccess) *cobra.Command { + o := NewGetUsersOptions(streams, configAccess) + + cmd := &cobra.Command{ + Use: "get-users", + Short: i18n.T("Display users defined in the kubeconfig"), + Long: "Display users defined in the kubeconfig.", + Example: getUsersExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Run()) + }, + } + + return cmd +} + +// Run performs the command +func (o *GetUsersOptions) Run() error { + config, err := o.configAccess.GetStartingConfig() + if err != nil { + return err + } + + users := make([]string, 0, len(config.AuthInfos)) + for user := range config.AuthInfos { + users = append(users, user) + } + sort.Strings(users) + + fmt.Fprintf(o.Out, "NAME\n") + for _, user := range users { + fmt.Fprintf(o.Out, "%s\n", user) + } + + return nil +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/config/get_users_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/config/get_users_test.go new file mode 100644 index 00000000000..4f9822828b0 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/config/get_users_test.go @@ -0,0 +1,75 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import ( + "testing" + + "k8s.io/cli-runtime/pkg/genericclioptions" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" +) + +func TestGetUsersRun(t *testing.T) { + var tests = []struct { + name string + config clientcmdapi.Config + expected string + }{ + { + name: "no users", + config: clientcmdapi.Config{}, + expected: "NAME\n", + }, + { + name: "some users", + config: clientcmdapi.Config{ + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "minikube": {Username: "minikube"}, + "admin": {Username: "admin"}, + }, + }, + expected: `NAME +admin +minikube +`, + }, + } + + for i := range tests { + test := tests[i] + t.Run(test.name, func(t *testing.T) { + tf := cmdtesting.NewTestFactory() + defer tf.Cleanup() + + ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() + pathOptions, err := tf.PathOptionsWithConfig(test.config) + if err != nil { + t.Fatalf("unexpected error executing command: %v", err) + } + options := NewGetUsersOptions(ioStreams, pathOptions) + + if err = options.Run(); err != nil { + t.Fatalf("unexpected error executing command: %v", err) + } + + if got := out.String(); got != test.expected { + t.Fatalf("expected: %s but got %s", test.expected, got) + } + }) + } +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/testing/fake.go b/staging/src/k8s.io/kubectl/pkg/cmd/testing/fake.go index d6b3007a89a..d3aa5b3e8c9 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/testing/fake.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/testing/fake.go @@ -465,6 +465,25 @@ func (f *TestFactory) ClientForMapping(mapping *meta.RESTMapping) (resource.REST return f.Client, nil } +// PathOptions returns a new PathOptions with a temp file +func (f *TestFactory) PathOptions() *clientcmd.PathOptions { + pathOptions := clientcmd.NewDefaultPathOptions() + pathOptions.GlobalFile = f.tempConfigFile.Name() + pathOptions.EnvVar = "" + return pathOptions +} + +// PathOptionsWithConfig writes a config to a temp file and returns PathOptions +func (f *TestFactory) PathOptionsWithConfig(config clientcmdapi.Config) (*clientcmd.PathOptions, error) { + pathOptions := f.PathOptions() + err := clientcmd.WriteToFile(config, pathOptions.GlobalFile) + if err != nil { + return nil, err + } + + return pathOptions, nil +} + // UnstructuredClientForMapping is used to get UnstructuredClient from a TestFactory func (f *TestFactory) UnstructuredClientForMapping(mapping *meta.RESTMapping) (resource.RESTClient, error) { if f.UnstructuredClientForMappingFunc != nil {