diff --git a/hack/verify-flags/known-flags.txt b/hack/verify-flags/known-flags.txt index 4deadf9ad69..c0797ce7e48 100644 --- a/hack/verify-flags/known-flags.txt +++ b/hack/verify-flags/known-flags.txt @@ -17,6 +17,8 @@ api-token api-version apiserver-count auth-path +auth-provider +auth-provider-arg authentication-token-webhook-cache-ttl authentication-token-webhook-config-file authorization-mode diff --git a/pkg/kubectl/cmd/config/create_authinfo.go b/pkg/kubectl/cmd/config/create_authinfo.go index 3b0669c4c4f..82a2947cfa9 100644 --- a/pkg/kubectl/cmd/config/create_authinfo.go +++ b/pkg/kubectl/cmd/config/create_authinfo.go @@ -29,6 +29,7 @@ import ( "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/util" "k8s.io/kubernetes/pkg/util/flag" ) @@ -43,8 +44,17 @@ type createAuthInfoOptions struct { username util.StringFlag password util.StringFlag embedCertData flag.Tristate + authProvider util.StringFlag + + authProviderArgs map[string]string + authProviderArgsToRemove []string } +const ( + flagAuthProvider = "auth-provider" + flagAuthProviderArg = "auth-provider-arg" +) + var ( create_authinfo_long = fmt.Sprintf(` Sets a user entry in kubeconfig @@ -71,19 +81,32 @@ Specifying a name that already exists will merge new fields on top of existing v kubectl config set-credentials cluster-admin --username=admin --password=uXFGweU9l35qcif # Embed client certificate data in the "cluster-admin" entry - kubectl config set-credentials cluster-admin --client-certificate=~/.kube/admin.crt --embed-certs=true`) + kubectl config set-credentials cluster-admin --client-certificate=~/.kube/admin.crt --embed-certs=true + + # Enable the Google Compute Platform auth provider for the "cluster-admin" entry + kubectl config set-credentials cluster-admin --auth-provider=gcp + + # Enable the OpenID Connect auth provider for the "cluster-admin" entry with additional args + 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-`) ) func NewCmdConfigSetAuthInfo(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { options := &createAuthInfoOptions{configAccess: configAccess} + return newCmdConfigSetAuthInfo(out, options) +} +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]", clientcmd.FlagCertFile, clientcmd.FlagKeyFile, clientcmd.FlagBearerToken, clientcmd.FlagUsername, clientcmd.FlagPassword), + 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), Short: "Sets a user entry in kubeconfig", Long: create_authinfo_long, Example: create_authinfo_example, Run: func(cmd *cobra.Command, args []string) { - if !options.complete(cmd) { + if !options.complete(cmd, out) { + cmd.Help() return } @@ -103,6 +126,8 @@ func NewCmdConfigSetAuthInfo(out io.Writer, configAccess clientcmd.ConfigAccess) cmd.Flags().Var(&options.token, clientcmd.FlagBearerToken, clientcmd.FlagBearerToken+" for the user entry in kubeconfig") cmd.Flags().Var(&options.username, clientcmd.FlagUsername, clientcmd.FlagUsername+" for the user entry in kubeconfig") 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' arugments for the auth provider") f := cmd.Flags().VarPF(&options.embedCertData, clientcmd.FlagEmbedCerts, "", "embed client cert/key for the user entry in kubeconfig") f.NoOptDefVal = "true" @@ -180,6 +205,28 @@ func (o *createAuthInfoOptions) modifyAuthInfo(existingAuthInfo clientcmdapi.Aut modifiedAuthInfo.Password = o.password.Value() setBasic = setBasic || len(modifiedAuthInfo.Password) > 0 } + if o.authProvider.Provided() { + newName := o.authProvider.Value() + + // Only overwrite if the existing auth-provider is nil, or different than the newly specified one. + if modifiedAuthInfo.AuthProvider == nil || modifiedAuthInfo.AuthProvider.Name != newName { + modifiedAuthInfo.AuthProvider = &clientcmdapi.AuthProviderConfig{ + Name: newName, + } + } + } + + if modifiedAuthInfo.AuthProvider != nil { + if modifiedAuthInfo.AuthProvider.Config == nil { + modifiedAuthInfo.AuthProvider.Config = make(map[string]string) + } + for _, toRemove := range o.authProviderArgsToRemove { + delete(modifiedAuthInfo.AuthProvider.Config, toRemove) + } + for key, value := range o.authProviderArgs { + modifiedAuthInfo.AuthProvider.Config[key] = value + } + } // If any auth info was set, make sure any other existing auth types are cleared if setToken || setBasic { @@ -195,13 +242,28 @@ func (o *createAuthInfoOptions) modifyAuthInfo(existingAuthInfo clientcmdapi.Aut return modifiedAuthInfo } -func (o *createAuthInfoOptions) complete(cmd *cobra.Command) bool { +func (o *createAuthInfoOptions) complete(cmd *cobra.Command, out io.Writer) bool { args := cmd.Flags().Args() if len(args) != 1 { - cmd.Help() return false } + authProviderArgs, err := cmd.Flags().GetStringSlice(flagAuthProviderArg) + if err != nil { + fmt.Fprintf(out, "Error: %s\n", err) + return false + } + + if len(authProviderArgs) > 0 { + newPairs, removePairs, err := cmdutil.ParsePairs(authProviderArgs, flagAuthProviderArg, true) + if err != nil { + fmt.Fprintf(out, "Error: %s\n", err) + return false + } + o.authProviderArgs = newPairs + o.authProviderArgsToRemove = removePairs + } + o.name = args[0] return true } diff --git a/pkg/kubectl/cmd/config/create_authinfo_test.go b/pkg/kubectl/cmd/config/create_authinfo_test.go new file mode 100644 index 00000000000..a973bdc510b --- /dev/null +++ b/pkg/kubectl/cmd/config/create_authinfo_test.go @@ -0,0 +1,190 @@ +/* +Copyright 2016 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 ( + "bytes" + "reflect" + "testing" + + "k8s.io/kubernetes/pkg/util" +) + +func stringFlagFor(s string) util.StringFlag { + var f util.StringFlag + f.Set(s) + return f +} + +func TestCreateAuthInfoOptions(t *testing.T) { + tests := []struct { + flags []string + wantParseErr bool + wantCompleteErr bool + wantValidateErr bool + + wantOptions *createAuthInfoOptions + }{ + { + flags: []string{ + "me", + }, + wantOptions: &createAuthInfoOptions{ + name: "me", + }, + }, + { + flags: []string{ + "me", + "--token=foo", + }, + wantOptions: &createAuthInfoOptions{ + name: "me", + token: stringFlagFor("foo"), + }, + }, + { + flags: []string{ + "me", + "--username=jane", + "--password=bar", + }, + wantOptions: &createAuthInfoOptions{ + name: "me", + username: stringFlagFor("jane"), + password: stringFlagFor("bar"), + }, + }, + { + // Cannot provide both token and basic auth. + flags: []string{ + "me", + "--token=foo", + "--username=jane", + "--password=bar", + }, + wantValidateErr: true, + }, + { + flags: []string{ + "--auth-provider=oidc", + "--auth-provider-arg=client-id=foo", + "--auth-provider-arg=client-secret=bar", + "me", + }, + wantOptions: &createAuthInfoOptions{ + name: "me", + authProvider: stringFlagFor("oidc"), + authProviderArgs: map[string]string{ + "client-id": "foo", + "client-secret": "bar", + }, + authProviderArgsToRemove: []string{}, + }, + }, + { + flags: []string{ + "--auth-provider=oidc", + "--auth-provider-arg=client-id-", + "--auth-provider-arg=client-secret-", + "me", + }, + wantOptions: &createAuthInfoOptions{ + name: "me", + authProvider: stringFlagFor("oidc"), + authProviderArgs: map[string]string{}, + authProviderArgsToRemove: []string{ + "client-id", + "client-secret", + }, + }, + }, + { + flags: []string{ + "--auth-provider-arg=client-id-", // auth provider name not required + "--auth-provider-arg=client-secret-", + "me", + }, + wantOptions: &createAuthInfoOptions{ + name: "me", + authProviderArgs: map[string]string{}, + authProviderArgsToRemove: []string{ + "client-id", + "client-secret", + }, + }, + }, + { + flags: []string{ + "--auth-provider=oidc", + "--auth-provider-arg=client-id", // values must be of form 'key=value' or 'key-' + "me", + }, + wantCompleteErr: true, + }, + { + flags: []string{ + // No name for authinfo provided. + }, + wantCompleteErr: true, + }, + } + + for i, test := range tests { + buff := new(bytes.Buffer) + + opts := new(createAuthInfoOptions) + cmd := newCmdConfigSetAuthInfo(buff, opts) + if err := cmd.ParseFlags(test.flags); err != nil { + if !test.wantParseErr { + t.Errorf("case %d: parsing error for flags %q: %v: %s", i, test.flags, err, buff) + } + continue + } + if test.wantParseErr { + t.Errorf("case %d: expected parsing error for flags %q: %s", i, test.flags, buff) + continue + } + + if !opts.complete(cmd, buff) { + if !test.wantCompleteErr { + t.Errorf("case %d: complete() error for flags %q: %s", i, test.flags, buff) + } + continue + } + if test.wantCompleteErr { + t.Errorf("case %d: complete() expected errors for flags %q: %s", i, test.flags, buff) + continue + } + + if err := opts.validate(); err != nil { + if !test.wantValidateErr { + t.Errorf("case %d: flags %q: validate failed: %v", i, test.flags, err) + } + continue + } + + if test.wantValidateErr { + t.Errorf("case %d: flags %q: expected validate to fail", i, test.flags) + continue + } + + if !reflect.DeepEqual(opts, test.wantOptions) { + t.Errorf("case %d: flags %q: mis-matched options,\nwanted=%#v\ngot= %#v", i, test.flags, test.wantOptions, opts) + } + } +}