diff --git a/pkg/kubectl/cmd/config/create_authinfo.go b/pkg/kubectl/cmd/config/create_authinfo.go index 475684b9247..13bbda2d5cd 100644 --- a/pkg/kubectl/cmd/config/create_authinfo.go +++ b/pkg/kubectl/cmd/config/create_authinfo.go @@ -48,11 +48,22 @@ type createAuthInfoOptions struct { authProviderArgs map[string]string authProviderArgsToRemove []string + + execCommand cliflag.StringFlag + execAPIVersion cliflag.StringFlag + execArgs []string + execEnv map[string]string + execEnvToRemove []string } const ( flagAuthProvider = "auth-provider" flagAuthProviderArg = "auth-provider-arg" + + flagExecCommand = "exec-command" + flagExecAPIVersion = "exec-api-version" + flagExecArg = "exec-arg" + flagExecEnv = "exec-env" ) var ( @@ -90,7 +101,19 @@ var ( kubectl config set-credentials cluster-admin --auth-provider=oidc --auth-provider-arg=client-id=foo --auth-provider-arg=client-secret=bar # Remove the "client-secret" config value for the OpenID Connect auth provider for the "cluster-admin" entry - kubectl config set-credentials cluster-admin --auth-provider=oidc --auth-provider-arg=client-secret-`) + kubectl config set-credentials cluster-admin --auth-provider=oidc --auth-provider-arg=client-secret- + + # Enable new exec auth plugin for the "cluster-admin" entry + kubectl config set-credentials cluster-admin --exec-command=/path/to/the/executable --exec-api-version=client.authentication.k8s.io/v1beta + + # Define new exec auth plugin args for the "cluster-admin" entry + kubectl config set-credentials cluster-admin --exec-arg=arg1 --exec-arg=arg2 + + # Create or update exec auth plugin environment variables for the "cluster-admin" entry + kubectl config set-credentials cluster-admin --exec-env=key1=val1 --exec-env=key2=val2 + + # Remove exec auth plugin environment variables for the "cluster-admin" entry + kubectl config set-credentials cluster-admin --exec-env=var-to-remove-`) ) // NewCmdConfigSetAuthInfo returns an Command option instance for 'config set-credentials' sub command @@ -101,7 +124,30 @@ func NewCmdConfigSetAuthInfo(out io.Writer, configAccess clientcmd.ConfigAccess) func newCmdConfigSetAuthInfo(out io.Writer, options *createAuthInfoOptions) *cobra.Command { cmd := &cobra.Command{ - Use: fmt.Sprintf("set-credentials NAME [--%v=path/to/certfile] [--%v=path/to/keyfile] [--%v=bearer_token] [--%v=basic_user] [--%v=basic_password] [--%v=provider_name] [--%v=key=value]", clientcmd.FlagCertFile, clientcmd.FlagKeyFile, clientcmd.FlagBearerToken, clientcmd.FlagUsername, clientcmd.FlagPassword, flagAuthProvider, flagAuthProviderArg), + Use: fmt.Sprintf( + "set-credentials NAME [--%v=path/to/certfile] "+ + "[--%v=path/to/keyfile] "+ + "[--%v=bearer_token] "+ + "[--%v=basic_user] "+ + "[--%v=basic_password] "+ + "[--%v=provider_name] "+ + "[--%v=key=value] "+ + "[--%v=exec_command] "+ + "[--%v=exec_api_version] "+ + "[--%v=arg] "+ + "[--%v=key=value]", + clientcmd.FlagCertFile, + clientcmd.FlagKeyFile, + clientcmd.FlagBearerToken, + clientcmd.FlagUsername, + clientcmd.FlagPassword, + flagAuthProvider, + flagAuthProviderArg, + flagExecCommand, + flagExecAPIVersion, + flagExecArg, + flagExecEnv, + ), DisableFlagsInUseLine: true, Short: i18n.T("Sets a user entry in kubeconfig"), Long: createAuthInfoLong, @@ -126,6 +172,10 @@ func newCmdConfigSetAuthInfo(out io.Writer, options *createAuthInfoOptions) *cob cmd.Flags().Var(&options.password, clientcmd.FlagPassword, clientcmd.FlagPassword+" for the user entry in kubeconfig") cmd.Flags().Var(&options.authProvider, flagAuthProvider, "Auth provider for the user entry in kubeconfig") cmd.Flags().StringSlice(flagAuthProviderArg, nil, "'key=value' arguments for the auth provider") + cmd.Flags().Var(&options.execCommand, flagExecCommand, "Command for the exec credential plugin for the user entry in kubeconfig") + cmd.Flags().Var(&options.execAPIVersion, flagExecAPIVersion, "API version of the exec credential plugin for the user entry in kubeconfig") + cmd.Flags().StringSlice(flagExecArg, nil, "New arguments for the exec credential plugin command for the user entry in kubeconfig") + cmd.Flags().StringArray(flagExecEnv, nil, "'key=value' environment values for the exec credential plugin") f := cmd.Flags().VarPF(&options.embedCertData, clientcmd.FlagEmbedCerts, "", "Embed client cert/key for the user entry in kubeconfig") f.NoOptDefVal = "true" @@ -226,6 +276,72 @@ func (o *createAuthInfoOptions) modifyAuthInfo(existingAuthInfo clientcmdapi.Aut } } + if o.execCommand.Provided() { + newExecCommand := o.execCommand.Value() + + // create new Exec if doesn't exist, otherwise just modify the command + if modifiedAuthInfo.Exec == nil { + modifiedAuthInfo.Exec = &clientcmdapi.ExecConfig{ + Command: newExecCommand, + } + } else { + modifiedAuthInfo.Exec.Command = newExecCommand + // explicitly reset exec arguments + modifiedAuthInfo.Exec.Args = nil + } + } + + // modify next values only if Exec exists, ignore these changes otherwise + if modifiedAuthInfo.Exec != nil { + if o.execAPIVersion.Provided() { + modifiedAuthInfo.Exec.APIVersion = o.execAPIVersion.Value() + } + + // rewrite exec arguments list with new values + if o.execArgs != nil { + modifiedAuthInfo.Exec.Args = o.execArgs + } + + // iterate over the existing exec env values and remove the specified + if o.execEnvToRemove != nil { + newExecEnv := []clientcmdapi.ExecEnvVar{} + for _, value := range modifiedAuthInfo.Exec.Env { + needToRemove := false + for _, elemToRemove := range o.execEnvToRemove { + if value.Name == elemToRemove { + needToRemove = true + break + } + } + if !needToRemove { + newExecEnv = append(newExecEnv, value) + } + } + modifiedAuthInfo.Exec.Env = newExecEnv + } + + // update or create specified environment variables for the exec plugin + if o.execEnv != nil { + newEnv := []clientcmdapi.ExecEnvVar{} + for newEnvName, newEnvValue := range o.execEnv { + needToCreate := true + for i := 0; i < len(modifiedAuthInfo.Exec.Env); i++ { + if modifiedAuthInfo.Exec.Env[i].Name == newEnvName { + // update the existing value + needToCreate = false + modifiedAuthInfo.Exec.Env[i].Value = newEnvValue + break + } + } + if needToCreate { + // create a new env value + newEnv = append(newEnv, clientcmdapi.ExecEnvVar{Name: newEnvName, Value: newEnvValue}) + } + } + modifiedAuthInfo.Exec.Env = append(modifiedAuthInfo.Exec.Env, newEnv...) + } + } + // If any auth info was set, make sure any other existing auth types are cleared if setToken || setBasic { if !setToken { @@ -260,6 +376,27 @@ func (o *createAuthInfoOptions) complete(cmd *cobra.Command, out io.Writer) erro o.authProviderArgsToRemove = removePairs } + execArgs, err := cmd.Flags().GetStringSlice(flagExecArg) + if err != nil { + return fmt.Errorf("Error: %s", err) + } + if len(execArgs) > 0 { + o.execArgs = execArgs + } + + execEnv, err := cmd.Flags().GetStringArray(flagExecEnv) + if err != nil { + return fmt.Errorf("Error: %s", err) + } + if len(execEnv) > 0 { + newPairs, removePairs, err := cmdutil.ParsePairs(execEnv, flagExecEnv, true) + if err != nil { + return fmt.Errorf("Error: %s", err) + } + o.execEnv = newPairs + o.execEnvToRemove = removePairs + } + o.name = args[0] return nil } diff --git a/pkg/kubectl/cmd/config/create_authinfo_test.go b/pkg/kubectl/cmd/config/create_authinfo_test.go index 7f7025b7bc1..45299050784 100644 --- a/pkg/kubectl/cmd/config/create_authinfo_test.go +++ b/pkg/kubectl/cmd/config/create_authinfo_test.go @@ -156,6 +156,48 @@ func TestCreateAuthInfoOptions(t *testing.T) { }, wantCompleteErr: true, }, + { + name: "test10", + flags: []string{ + "--exec-command=example-client-go-exec-plugin", + "me", + }, + wantOptions: &createAuthInfoOptions{ + name: "me", + execCommand: stringFlagFor("example-client-go-exec-plugin"), + }, + }, + { + name: "test11", + flags: []string{ + "--exec-command=example-client-go-exec-plugin", + "--exec-arg=arg1", + "--exec-arg=arg2", + "me", + }, + wantOptions: &createAuthInfoOptions{ + name: "me", + execCommand: stringFlagFor("example-client-go-exec-plugin"), + execArgs: []string{"arg1", "arg2"}, + }, + }, + { + name: "test12", + flags: []string{ + "--exec-command=example-client-go-exec-plugin", + "--exec-env=key1=val1", + "--exec-env=key2=val2", + "--exec-env=env-remove1-", + "--exec-env=env-remove2-", + "me", + }, + wantOptions: &createAuthInfoOptions{ + name: "me", + execCommand: stringFlagFor("example-client-go-exec-plugin"), + execEnv: map[string]string{"key1": "val1", "key2": "val2"}, + execEnvToRemove: []string{"env-remove1", "env-remove2"}, + }, + }, } for _, tt := range tests { @@ -205,6 +247,185 @@ func TestCreateAuthInfoOptions(t *testing.T) { } } +func TestModifyExistingAuthInfo(t *testing.T) { + tests := []struct { + name string + flags []string + wantParseErr bool + wantCompleteErr bool + wantValidateErr bool + + existingAuthInfo clientcmdapi.AuthInfo + wantAuthInfo clientcmdapi.AuthInfo + }{ + { + name: "1. create new exec config", + flags: []string{ + "--exec-command=example-client-go-exec-plugin", + "--exec-api-version=client.authentication.k8s.io/v1", + "me", + }, + existingAuthInfo: clientcmdapi.AuthInfo{}, + wantAuthInfo: clientcmdapi.AuthInfo{ + Exec: &clientcmdapi.ExecConfig{ + Command: "example-client-go-exec-plugin", + APIVersion: "client.authentication.k8s.io/v1", + }, + }, + }, + { + name: "2. redefine exec args", + flags: []string{ + "--exec-arg=new-arg1", + "--exec-arg=new-arg2", + "me", + }, + existingAuthInfo: clientcmdapi.AuthInfo{ + Exec: &clientcmdapi.ExecConfig{ + Command: "example-client-go-exec-plugin", + APIVersion: "client.authentication.k8s.io/v1beta1", + Args: []string{"existing-arg1", "existing-arg2"}, + }, + }, + wantAuthInfo: clientcmdapi.AuthInfo{ + Exec: &clientcmdapi.ExecConfig{ + Command: "example-client-go-exec-plugin", + APIVersion: "client.authentication.k8s.io/v1beta1", + Args: []string{"new-arg1", "new-arg2"}, + }, + }, + }, + { + name: "3. reset exec args", + flags: []string{ + "--exec-command=example-client-go-exec-plugin", + "me", + }, + existingAuthInfo: clientcmdapi.AuthInfo{ + Exec: &clientcmdapi.ExecConfig{ + Command: "example-client-go-exec-plugin", + APIVersion: "client.authentication.k8s.io/v1beta1", + Args: []string{"existing-arg1", "existing-arg2"}, + }, + }, + wantAuthInfo: clientcmdapi.AuthInfo{ + Exec: &clientcmdapi.ExecConfig{ + Command: "example-client-go-exec-plugin", + APIVersion: "client.authentication.k8s.io/v1beta1", + }, + }, + }, + { + name: "4. modify exec env variables", + flags: []string{ + "--exec-command=example-client-go-exec-plugin", + "--exec-env=name1=value1000", + "--exec-env=name3=value3", + "--exec-env=name2-", + "--exec-env=non-existing-", + "me", + }, + existingAuthInfo: clientcmdapi.AuthInfo{ + Exec: &clientcmdapi.ExecConfig{ + Command: "existing-command", + APIVersion: "client.authentication.k8s.io/v1beta1", + Env: []clientcmdapi.ExecEnvVar{ + {Name: "name1", Value: "value1"}, + {Name: "name2", Value: "value2"}, + }, + }, + }, + wantAuthInfo: clientcmdapi.AuthInfo{ + Exec: &clientcmdapi.ExecConfig{ + Command: "example-client-go-exec-plugin", + APIVersion: "client.authentication.k8s.io/v1beta1", + Env: []clientcmdapi.ExecEnvVar{ + {Name: "name1", Value: "value1000"}, + {Name: "name3", Value: "value3"}, + }, + }, + }, + }, + { + name: "5. modify auth provider arguments", + flags: []string{ + "--auth-provider=new-auth-provider", + "--auth-provider-arg=key1=val1000", + "--auth-provider-arg=key3=val3", + "--auth-provider-arg=key2-", + "--auth-provider-arg=non-existing-", + "me", + }, + existingAuthInfo: clientcmdapi.AuthInfo{ + AuthProvider: &clientcmdapi.AuthProviderConfig{ + Name: "auth-provider", + Config: map[string]string{ + "key1": "val1", + "key2": "val2", + }, + }, + }, + wantAuthInfo: clientcmdapi.AuthInfo{ + AuthProvider: &clientcmdapi.AuthProviderConfig{ + Name: "new-auth-provider", + Config: map[string]string{ + "key1": "val1000", + "key3": "val3", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buff := new(bytes.Buffer) + + opts := new(createAuthInfoOptions) + cmd := newCmdConfigSetAuthInfo(buff, opts) + if err := cmd.ParseFlags(tt.flags); err != nil { + if !tt.wantParseErr { + t.Errorf("case %s: parsing error for flags %q: %v: %s", tt.name, tt.flags, err, buff) + } + return + } + if tt.wantParseErr { + t.Errorf("case %s: expected parsing error for flags %q: %s", tt.name, tt.flags, buff) + return + } + + if err := opts.complete(cmd, buff); err != nil { + if !tt.wantCompleteErr { + t.Errorf("case %s: complete() error for flags %q: %s", tt.name, tt.flags, buff) + } + return + } + if tt.wantCompleteErr { + t.Errorf("case %s: complete() expected errors for flags %q: %s", tt.name, tt.flags, buff) + return + } + + if err := opts.validate(); err != nil { + if !tt.wantValidateErr { + t.Errorf("case %s: flags %q: validate failed: %v", tt.name, tt.flags, err) + } + return + } + + if tt.wantValidateErr { + t.Errorf("case %s: flags %q: expected validate to fail", tt.name, tt.flags) + return + } + + modifiedAuthInfo := opts.modifyAuthInfo(tt.existingAuthInfo) + + if !reflect.DeepEqual(modifiedAuthInfo, tt.wantAuthInfo) { + t.Errorf("case %s: flags %q: mis-matched auth info,\nwanted=%#v\ngot= %#v", tt.name, tt.flags, tt.wantAuthInfo, modifiedAuthInfo) + } + }) + } +} + type createAuthInfoTest struct { description string config clientcmdapi.Config