Add get-users and delete-user to kubectl config

Signed-off-by: Eddie Zaneski <eddiezane@gmail.com>

Kubernetes-commit: 95b189fd8f69812738ba9ea472237ab946863d23
This commit is contained in:
Eddie Zaneski 2020-04-03 16:41:46 -06:00 committed by Kubernetes Publisher
parent 543e326352
commit 951c0caa13
6 changed files with 508 additions and 0 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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 {