diff --git a/pkg/cmd/config/config.go b/pkg/cmd/config/config.go index 8ec65294..7848c396 100644 --- a/pkg/cmd/config/config.go +++ b/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/pkg/cmd/config/delete_user.go b/pkg/cmd/config/delete_user.go new file mode 100644 index 00000000..66f709bc --- /dev/null +++ b/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/pkg/cmd/config/delete_user_test.go b/pkg/cmd/config/delete_user_test.go new file mode 100644 index 00000000..94a4f4d7 --- /dev/null +++ b/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/pkg/cmd/config/get_users.go b/pkg/cmd/config/get_users.go new file mode 100644 index 00000000..6feb4abf --- /dev/null +++ b/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/pkg/cmd/config/get_users_test.go b/pkg/cmd/config/get_users_test.go new file mode 100644 index 00000000..4f982282 --- /dev/null +++ b/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/pkg/cmd/testing/fake.go b/pkg/cmd/testing/fake.go index 3f160e4d..636ab639 100644 --- a/pkg/cmd/testing/fake.go +++ b/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 {