Add command to request a bound service account token
This commit is contained in:
		@@ -41,6 +41,7 @@ require (
 | 
				
			|||||||
	k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65
 | 
						k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65
 | 
				
			||||||
	k8s.io/metrics v0.0.0
 | 
						k8s.io/metrics v0.0.0
 | 
				
			||||||
	k8s.io/utils v0.0.0-20211208161948-7d6a63dca704
 | 
						k8s.io/utils v0.0.0-20211208161948-7d6a63dca704
 | 
				
			||||||
 | 
						sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2
 | 
				
			||||||
	sigs.k8s.io/kustomize/kustomize/v4 v4.4.1
 | 
						sigs.k8s.io/kustomize/kustomize/v4 v4.4.1
 | 
				
			||||||
	sigs.k8s.io/kustomize/kyaml v0.13.0
 | 
						sigs.k8s.io/kustomize/kyaml v0.13.0
 | 
				
			||||||
	sigs.k8s.io/yaml v1.2.0
 | 
						sigs.k8s.io/yaml v1.2.0
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -153,6 +153,7 @@ func NewCmdCreate(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cob
 | 
				
			|||||||
	cmd.AddCommand(NewCmdCreateJob(f, ioStreams))
 | 
						cmd.AddCommand(NewCmdCreateJob(f, ioStreams))
 | 
				
			||||||
	cmd.AddCommand(NewCmdCreateCronJob(f, ioStreams))
 | 
						cmd.AddCommand(NewCmdCreateCronJob(f, ioStreams))
 | 
				
			||||||
	cmd.AddCommand(NewCmdCreateIngress(f, ioStreams))
 | 
						cmd.AddCommand(NewCmdCreateIngress(f, ioStreams))
 | 
				
			||||||
 | 
						cmd.AddCommand(NewCmdCreateToken(f, ioStreams))
 | 
				
			||||||
	return cmd
 | 
						return cmd
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										263
									
								
								staging/src/k8s.io/kubectl/pkg/cmd/create/create_token.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										263
									
								
								staging/src/k8s.io/kubectl/pkg/cmd/create/create_token.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,263 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright 2022 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 create
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/spf13/cobra"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						authenticationv1 "k8s.io/api/authentication/v1"
 | 
				
			||||||
 | 
						metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/runtime"
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/types"
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/util/sets"
 | 
				
			||||||
 | 
						"k8s.io/cli-runtime/pkg/genericclioptions"
 | 
				
			||||||
 | 
						corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
 | 
				
			||||||
 | 
						cmdutil "k8s.io/kubectl/pkg/cmd/util"
 | 
				
			||||||
 | 
						"k8s.io/kubectl/pkg/scheme"
 | 
				
			||||||
 | 
						"k8s.io/kubectl/pkg/util"
 | 
				
			||||||
 | 
						"k8s.io/kubectl/pkg/util/templates"
 | 
				
			||||||
 | 
						"k8s.io/kubectl/pkg/util/term"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TokenOptions is the data required to perform a token request operation.
 | 
				
			||||||
 | 
					type TokenOptions struct {
 | 
				
			||||||
 | 
						// PrintFlags holds options necessary for obtaining a printer
 | 
				
			||||||
 | 
						PrintFlags *genericclioptions.PrintFlags
 | 
				
			||||||
 | 
						PrintObj   func(obj runtime.Object) error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Name and namespace of service account to create a token for
 | 
				
			||||||
 | 
						Name      string
 | 
				
			||||||
 | 
						Namespace string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// BoundObjectKind is the kind of object to bind the token to. Optional. Can be Pod or Secret.
 | 
				
			||||||
 | 
						BoundObjectKind string
 | 
				
			||||||
 | 
						// BoundObjectName is the name of the object to bind the token to. Required if BoundObjectKind is set.
 | 
				
			||||||
 | 
						BoundObjectName string
 | 
				
			||||||
 | 
						// BoundObjectUID is the uid of the object to bind the token to. If unset, defaults to the current uid of the bound object.
 | 
				
			||||||
 | 
						BoundObjectUID string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Audiences indicate the valid audiences for the requested token. If unset, defaults to the Kubernetes API server audiences.
 | 
				
			||||||
 | 
						Audiences []string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// ExpirationSeconds is the requested token lifetime. Optional.
 | 
				
			||||||
 | 
						ExpirationSeconds int64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// CoreClient is the API client used to request the token. Required.
 | 
				
			||||||
 | 
						CoreClient corev1client.CoreV1Interface
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// IOStreams are the output streams for the operation. Required.
 | 
				
			||||||
 | 
						genericclioptions.IOStreams
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						tokenLong = templates.LongDesc(`Request a service account token.`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tokenExample = templates.Examples(`
 | 
				
			||||||
 | 
							# Request a token to authenticate to the kube-apiserver as the service account "myapp" in the current namespace
 | 
				
			||||||
 | 
							kubectl create token myapp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							# Request a token for a service account in a custom namespace
 | 
				
			||||||
 | 
							kubectl create token myapp --namespace myns
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							# Request a token with a custom expiration
 | 
				
			||||||
 | 
							kubectl create token myapp --expiration-seconds 600
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							# Request a token with a custom audience
 | 
				
			||||||
 | 
							kubectl create token myapp --audience https://example.com
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							# Request a token bound to an instance of a Secret object
 | 
				
			||||||
 | 
							kubectl create token myapp --bound-object-kind Secret --bound-object-name mysecret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							# Request a token bound to an instance of a Secret object with a specific uid
 | 
				
			||||||
 | 
							kubectl create token myapp --bound-object-kind Secret --bound-object-name mysecret --bound-object-uid 0d4691ed-659b-4935-a832-355f77ee47cc
 | 
				
			||||||
 | 
					`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						boundObjectKindToAPIVersion = map[string]string{
 | 
				
			||||||
 | 
							"Pod":    "v1",
 | 
				
			||||||
 | 
							"Secret": "v1",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func NewTokenOpts(ioStreams genericclioptions.IOStreams) *TokenOptions {
 | 
				
			||||||
 | 
						return &TokenOptions{
 | 
				
			||||||
 | 
							PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
 | 
				
			||||||
 | 
							IOStreams:  ioStreams,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewCmdCreateToken returns an initialized Command for 'create token' sub command
 | 
				
			||||||
 | 
					func NewCmdCreateToken(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
 | 
				
			||||||
 | 
						o := NewTokenOpts(ioStreams)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cmd := &cobra.Command{
 | 
				
			||||||
 | 
							Use:                   "token SERVICE_ACCOUNT_NAME",
 | 
				
			||||||
 | 
							DisableFlagsInUseLine: true,
 | 
				
			||||||
 | 
							Short:                 "Request a service account token",
 | 
				
			||||||
 | 
							Long:                  tokenLong,
 | 
				
			||||||
 | 
							Example:               tokenExample,
 | 
				
			||||||
 | 
							ValidArgsFunction:     util.ResourceNameCompletionFunc(f, "serviceaccount"),
 | 
				
			||||||
 | 
							Run: func(cmd *cobra.Command, args []string) {
 | 
				
			||||||
 | 
								if err := o.Complete(f, cmd, args); err != nil {
 | 
				
			||||||
 | 
									cmdutil.CheckErr(err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if err := o.Validate(); err != nil {
 | 
				
			||||||
 | 
									cmdutil.CheckErr(err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if err := o.Run(); err != nil {
 | 
				
			||||||
 | 
									cmdutil.CheckErr(err)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						o.PrintFlags.AddFlags(cmd)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cmd.Flags().StringArrayVar(&o.Audiences, "audience", o.Audiences, "Audience of the requested token. If unset, defaults to requesting a token for use with the Kubernetes API server. May be repeated to request a token valid for multiple audiences.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cmd.Flags().Int64Var(&o.ExpirationSeconds, "expiration-seconds", o.ExpirationSeconds, "Requested lifetime of the issued token. The server may return a token with a longer or shorter lifetime.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cmd.Flags().StringVar(&o.BoundObjectKind, "bound-object-kind", o.BoundObjectKind, "Kind of an object to bind the token to. "+
 | 
				
			||||||
 | 
							"Supported kinds are "+strings.Join(sets.StringKeySet(boundObjectKindToAPIVersion).List(), ", ")+". "+
 | 
				
			||||||
 | 
							"If set, --bound-object-name must be provided.")
 | 
				
			||||||
 | 
						cmd.Flags().StringVar(&o.BoundObjectName, "bound-object-name", o.BoundObjectName, "Name of an object to bind the token to. "+
 | 
				
			||||||
 | 
							"The token will expire when the object is deleted. "+
 | 
				
			||||||
 | 
							"Requires --bound-object-kind.")
 | 
				
			||||||
 | 
						cmd.Flags().StringVar(&o.BoundObjectUID, "bound-object-uid", o.BoundObjectUID, "UID of an object to bind the token to. "+
 | 
				
			||||||
 | 
							"Requires --bound-object-kind and --bound-object-name. "+
 | 
				
			||||||
 | 
							"If unset, the UID of the existing object is used.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return cmd
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Complete completes all the required options
 | 
				
			||||||
 | 
					func (o *TokenOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
 | 
				
			||||||
 | 
						var err error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						o.Name, err = NameFromCommandArgs(cmd, args)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						client, err := f.KubernetesClientSet()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						o.CoreClient = client.CoreV1()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						printer, err := o.PrintFlags.ToPrinter()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						o.PrintObj = func(obj runtime.Object) error {
 | 
				
			||||||
 | 
							return printer.PrintObj(obj, o.Out)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Validate makes sure provided values for TokenOptions are valid
 | 
				
			||||||
 | 
					func (o *TokenOptions) Validate() error {
 | 
				
			||||||
 | 
						if o.CoreClient == nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("no client provided")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(o.Name) == 0 {
 | 
				
			||||||
 | 
							return fmt.Errorf("service account name is required")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(o.Namespace) == 0 {
 | 
				
			||||||
 | 
							return fmt.Errorf("--namespace is required")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if o.ExpirationSeconds < 0 {
 | 
				
			||||||
 | 
							return fmt.Errorf("--expiration-seconds must be positive")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, aud := range o.Audiences {
 | 
				
			||||||
 | 
							if len(aud) == 0 {
 | 
				
			||||||
 | 
								return fmt.Errorf("--audience must not be an empty string")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(o.BoundObjectKind) == 0 {
 | 
				
			||||||
 | 
							if len(o.BoundObjectName) > 0 {
 | 
				
			||||||
 | 
								return fmt.Errorf("--bound-object-name can only be set if --bound-object-kind is provided")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(o.BoundObjectUID) > 0 {
 | 
				
			||||||
 | 
								return fmt.Errorf("--bound-object-uid can only be set if --bound-object-kind is provided")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							if _, ok := boundObjectKindToAPIVersion[o.BoundObjectKind]; !ok {
 | 
				
			||||||
 | 
								return fmt.Errorf("supported --bound-object-kind values are %s", strings.Join(sets.StringKeySet(boundObjectKindToAPIVersion).List(), ", "))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(o.BoundObjectName) == 0 {
 | 
				
			||||||
 | 
								return fmt.Errorf("--bound-object-name is required if --bound-object-kind is provided")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Run requests a token
 | 
				
			||||||
 | 
					func (o *TokenOptions) Run() error {
 | 
				
			||||||
 | 
						request := &authenticationv1.TokenRequest{
 | 
				
			||||||
 | 
							Spec: authenticationv1.TokenRequestSpec{
 | 
				
			||||||
 | 
								Audiences: o.Audiences,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if o.ExpirationSeconds > 0 {
 | 
				
			||||||
 | 
							request.Spec.ExpirationSeconds = &o.ExpirationSeconds
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(o.BoundObjectKind) > 0 {
 | 
				
			||||||
 | 
							request.Spec.BoundObjectRef = &authenticationv1.BoundObjectReference{
 | 
				
			||||||
 | 
								Kind:       o.BoundObjectKind,
 | 
				
			||||||
 | 
								APIVersion: boundObjectKindToAPIVersion[o.BoundObjectKind],
 | 
				
			||||||
 | 
								Name:       o.BoundObjectName,
 | 
				
			||||||
 | 
								UID:        types.UID(o.BoundObjectUID),
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						response, err := o.CoreClient.ServiceAccounts(o.Namespace).CreateToken(context.TODO(), o.Name, request, metav1.CreateOptions{})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to create token: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(response.Status.Token) == 0 {
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to create token: no token in server response")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if o.PrintFlags.OutputFlagSpecified() {
 | 
				
			||||||
 | 
							return o.PrintObj(response)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if term.IsTerminal(o.Out) {
 | 
				
			||||||
 | 
							// include a newline when printing interactively
 | 
				
			||||||
 | 
							fmt.Fprintf(o.Out, "%s\n", response.Status.Token)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// otherwise just print the token
 | 
				
			||||||
 | 
							fmt.Fprintf(o.Out, "%s", response.Status.Token)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										330
									
								
								staging/src/k8s.io/kubectl/pkg/cmd/create/create_token_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										330
									
								
								staging/src/k8s.io/kubectl/pkg/cmd/create/create_token_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,330 @@
 | 
				
			|||||||
 | 
					/*
 | 
				
			||||||
 | 
					Copyright 2022 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 create
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"io/ioutil"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"reflect"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/google/go-cmp/cmp"
 | 
				
			||||||
 | 
						"k8s.io/utils/pointer"
 | 
				
			||||||
 | 
						kjson "sigs.k8s.io/json"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						authenticationv1 "k8s.io/api/authentication/v1"
 | 
				
			||||||
 | 
						apierrors "k8s.io/apimachinery/pkg/api/errors"
 | 
				
			||||||
 | 
						metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
				
			||||||
 | 
						"k8s.io/apimachinery/pkg/runtime/schema"
 | 
				
			||||||
 | 
						"k8s.io/cli-runtime/pkg/genericclioptions"
 | 
				
			||||||
 | 
						"k8s.io/client-go/rest/fake"
 | 
				
			||||||
 | 
						cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
 | 
				
			||||||
 | 
						cmdutil "k8s.io/kubectl/pkg/cmd/util"
 | 
				
			||||||
 | 
						"k8s.io/kubectl/pkg/scheme"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCreateToken(t *testing.T) {
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							test string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							name              string
 | 
				
			||||||
 | 
							namespace         string
 | 
				
			||||||
 | 
							output            string
 | 
				
			||||||
 | 
							boundObjectKind   string
 | 
				
			||||||
 | 
							boundObjectName   string
 | 
				
			||||||
 | 
							boundObjectUID    string
 | 
				
			||||||
 | 
							audiences         []string
 | 
				
			||||||
 | 
							expirationSeconds int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							serverResponseToken string
 | 
				
			||||||
 | 
							serverResponseError string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							expectRequestPath  string
 | 
				
			||||||
 | 
							expectTokenRequest *authenticationv1.TokenRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							expectStdout string
 | 
				
			||||||
 | 
							expectStderr string
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								test: "simple",
 | 
				
			||||||
 | 
								name: "mysa",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
 | 
				
			||||||
 | 
								expectTokenRequest: &authenticationv1.TokenRequest{
 | 
				
			||||||
 | 
									TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								serverResponseToken: "abc",
 | 
				
			||||||
 | 
								expectStdout:        "abc",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								test:      "custom namespace",
 | 
				
			||||||
 | 
								name:      "custom-sa",
 | 
				
			||||||
 | 
								namespace: "custom-ns",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								expectRequestPath: "/api/v1/namespaces/custom-ns/serviceaccounts/custom-sa/token",
 | 
				
			||||||
 | 
								expectTokenRequest: &authenticationv1.TokenRequest{
 | 
				
			||||||
 | 
									TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								serverResponseToken: "abc",
 | 
				
			||||||
 | 
								expectStdout:        "abc",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								test:   "yaml",
 | 
				
			||||||
 | 
								name:   "mysa",
 | 
				
			||||||
 | 
								output: "yaml",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
 | 
				
			||||||
 | 
								expectTokenRequest: &authenticationv1.TokenRequest{
 | 
				
			||||||
 | 
									TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								serverResponseToken: "abc",
 | 
				
			||||||
 | 
								expectStdout: `apiVersion: authentication.k8s.io/v1
 | 
				
			||||||
 | 
					kind: TokenRequest
 | 
				
			||||||
 | 
					metadata:
 | 
				
			||||||
 | 
					  creationTimestamp: null
 | 
				
			||||||
 | 
					spec:
 | 
				
			||||||
 | 
					  audiences: null
 | 
				
			||||||
 | 
					  boundObjectRef: null
 | 
				
			||||||
 | 
					  expirationSeconds: null
 | 
				
			||||||
 | 
					status:
 | 
				
			||||||
 | 
					  expirationTimestamp: null
 | 
				
			||||||
 | 
					  token: abc
 | 
				
			||||||
 | 
					`,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								test:            "bad bound object kind",
 | 
				
			||||||
 | 
								name:            "mysa",
 | 
				
			||||||
 | 
								boundObjectKind: "Foo",
 | 
				
			||||||
 | 
								expectStderr:    `error: supported --bound-object-kind values are Pod, Secret`,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								test:            "missing bound object name",
 | 
				
			||||||
 | 
								name:            "mysa",
 | 
				
			||||||
 | 
								boundObjectKind: "Pod",
 | 
				
			||||||
 | 
								expectStderr:    `error: --bound-object-name is required if --bound-object-kind is provided`,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								test:            "invalid bound object name",
 | 
				
			||||||
 | 
								name:            "mysa",
 | 
				
			||||||
 | 
								boundObjectName: "mypod",
 | 
				
			||||||
 | 
								expectStderr:    `error: --bound-object-name can only be set if --bound-object-kind is provided`,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								test:           "invalid bound object uid",
 | 
				
			||||||
 | 
								name:           "mysa",
 | 
				
			||||||
 | 
								boundObjectUID: "myuid",
 | 
				
			||||||
 | 
								expectStderr:   `error: --bound-object-uid can only be set if --bound-object-kind is provided`,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								test: "valid bound object",
 | 
				
			||||||
 | 
								name: "mysa",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								boundObjectKind: "Pod",
 | 
				
			||||||
 | 
								boundObjectName: "mypod",
 | 
				
			||||||
 | 
								boundObjectUID:  "myuid",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
 | 
				
			||||||
 | 
								expectTokenRequest: &authenticationv1.TokenRequest{
 | 
				
			||||||
 | 
									TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
 | 
				
			||||||
 | 
									Spec: authenticationv1.TokenRequestSpec{
 | 
				
			||||||
 | 
										BoundObjectRef: &authenticationv1.BoundObjectReference{
 | 
				
			||||||
 | 
											Kind:       "Pod",
 | 
				
			||||||
 | 
											APIVersion: "v1",
 | 
				
			||||||
 | 
											Name:       "mypod",
 | 
				
			||||||
 | 
											UID:        "myuid",
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								serverResponseToken: "abc",
 | 
				
			||||||
 | 
								expectStdout:        "abc",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								test:         "invalid audience",
 | 
				
			||||||
 | 
								name:         "mysa",
 | 
				
			||||||
 | 
								audiences:    []string{"test", "", "test2"},
 | 
				
			||||||
 | 
								expectStderr: `error: --audience must not be an empty string`,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								test: "valid audiences",
 | 
				
			||||||
 | 
								name: "mysa",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								audiences: []string{"test,value1", "test,value2"},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
 | 
				
			||||||
 | 
								expectTokenRequest: &authenticationv1.TokenRequest{
 | 
				
			||||||
 | 
									TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
 | 
				
			||||||
 | 
									Spec: authenticationv1.TokenRequestSpec{
 | 
				
			||||||
 | 
										Audiences: []string{"test,value1", "test,value2"},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								serverResponseToken: "abc",
 | 
				
			||||||
 | 
								expectStdout:        "abc",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								test:              "invalid expiration",
 | 
				
			||||||
 | 
								name:              "mysa",
 | 
				
			||||||
 | 
								expirationSeconds: -1,
 | 
				
			||||||
 | 
								expectStderr:      `error: --expiration-seconds must be positive`,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								test: "valid expiration",
 | 
				
			||||||
 | 
								name: "mysa",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								expirationSeconds: 1000,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
 | 
				
			||||||
 | 
								expectTokenRequest: &authenticationv1.TokenRequest{
 | 
				
			||||||
 | 
									TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
 | 
				
			||||||
 | 
									Spec: authenticationv1.TokenRequestSpec{
 | 
				
			||||||
 | 
										ExpirationSeconds: pointer.Int64(1000),
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								serverResponseToken: "abc",
 | 
				
			||||||
 | 
								expectStdout:        "abc",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								test: "server error",
 | 
				
			||||||
 | 
								name: "mysa",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
 | 
				
			||||||
 | 
								expectTokenRequest: &authenticationv1.TokenRequest{
 | 
				
			||||||
 | 
									TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								serverResponseError: "bad bad request",
 | 
				
			||||||
 | 
								expectStderr:        `error: failed to create token:  "bad bad request" is invalid`,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								test: "server missing token",
 | 
				
			||||||
 | 
								name: "mysa",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
 | 
				
			||||||
 | 
								expectTokenRequest: &authenticationv1.TokenRequest{
 | 
				
			||||||
 | 
									TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								serverResponseToken: "",
 | 
				
			||||||
 | 
								expectStderr:        `error: failed to create token: no token in server response`,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, test := range tests {
 | 
				
			||||||
 | 
							t.Run(test.test, func(t *testing.T) {
 | 
				
			||||||
 | 
								defer cmdutil.DefaultBehaviorOnFatal()
 | 
				
			||||||
 | 
								sawError := ""
 | 
				
			||||||
 | 
								cmdutil.BehaviorOnFatal(func(str string, code int) {
 | 
				
			||||||
 | 
									sawError = str
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								namespace := "test"
 | 
				
			||||||
 | 
								if test.namespace != "" {
 | 
				
			||||||
 | 
									namespace = test.namespace
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								tf := cmdtesting.NewTestFactory().WithNamespace(namespace)
 | 
				
			||||||
 | 
								defer tf.Cleanup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								tf.Client = &fake.RESTClient{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								var code int
 | 
				
			||||||
 | 
								var body []byte
 | 
				
			||||||
 | 
								if len(test.serverResponseError) > 0 {
 | 
				
			||||||
 | 
									code = 422
 | 
				
			||||||
 | 
									response := apierrors.NewInvalid(schema.GroupKind{Group: "", Kind: ""}, test.serverResponseError, nil)
 | 
				
			||||||
 | 
									response.ErrStatus.APIVersion = "v1"
 | 
				
			||||||
 | 
									response.ErrStatus.Kind = "Status"
 | 
				
			||||||
 | 
									body, _ = json.Marshal(response.ErrStatus)
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									code = 200
 | 
				
			||||||
 | 
									response := authenticationv1.TokenRequest{
 | 
				
			||||||
 | 
										TypeMeta: metav1.TypeMeta{
 | 
				
			||||||
 | 
											APIVersion: "authentication.k8s.io/v1",
 | 
				
			||||||
 | 
											Kind:       "TokenRequest",
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
										Status: authenticationv1.TokenRequestStatus{Token: test.serverResponseToken},
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									body, _ = json.Marshal(response)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								ns := scheme.Codecs.WithoutConversion()
 | 
				
			||||||
 | 
								var tokenRequest *authenticationv1.TokenRequest
 | 
				
			||||||
 | 
								tf.Client = &fake.RESTClient{
 | 
				
			||||||
 | 
									NegotiatedSerializer: ns,
 | 
				
			||||||
 | 
									Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
 | 
				
			||||||
 | 
										if req.URL.Path != test.expectRequestPath {
 | 
				
			||||||
 | 
											t.Fatalf("expected %q, got %q", test.expectRequestPath, req.URL.Path)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										data, err := ioutil.ReadAll(req.Body)
 | 
				
			||||||
 | 
										if err != nil {
 | 
				
			||||||
 | 
											t.Fatal(err)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										tokenRequest = &authenticationv1.TokenRequest{}
 | 
				
			||||||
 | 
										if strictErrs, err := kjson.UnmarshalStrict(data, tokenRequest); err != nil {
 | 
				
			||||||
 | 
											t.Fatal(err)
 | 
				
			||||||
 | 
										} else if len(strictErrs) > 0 {
 | 
				
			||||||
 | 
											t.Fatal(strictErrs)
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										return &http.Response{
 | 
				
			||||||
 | 
											StatusCode: code,
 | 
				
			||||||
 | 
											Body:       ioutil.NopCloser(bytes.NewBuffer(body)),
 | 
				
			||||||
 | 
										}, nil
 | 
				
			||||||
 | 
									}),
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								ioStreams, _, stdout, _ := genericclioptions.NewTestIOStreams()
 | 
				
			||||||
 | 
								cmd := NewCmdCreateToken(tf, ioStreams)
 | 
				
			||||||
 | 
								if test.output != "" {
 | 
				
			||||||
 | 
									cmd.Flags().Set("output", test.output)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if test.boundObjectKind != "" {
 | 
				
			||||||
 | 
									cmd.Flags().Set("bound-object-kind", test.boundObjectKind)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if test.boundObjectName != "" {
 | 
				
			||||||
 | 
									cmd.Flags().Set("bound-object-name", test.boundObjectName)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if test.boundObjectUID != "" {
 | 
				
			||||||
 | 
									cmd.Flags().Set("bound-object-uid", test.boundObjectUID)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								for _, aud := range test.audiences {
 | 
				
			||||||
 | 
									cmd.Flags().Set("audience", aud)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if test.expirationSeconds != 0 {
 | 
				
			||||||
 | 
									cmd.Flags().Set("expiration-seconds", strconv.Itoa(test.expirationSeconds))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								cmd.Run(cmd, []string{test.name})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if !reflect.DeepEqual(tokenRequest, test.expectTokenRequest) {
 | 
				
			||||||
 | 
									t.Fatalf("unexpected request:\n%s", cmp.Diff(test.expectTokenRequest, tokenRequest))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if stdout.String() != test.expectStdout {
 | 
				
			||||||
 | 
									t.Errorf("unexpected stdout:\n%s", cmp.Diff(test.expectStdout, stdout.String()))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if sawError != test.expectStderr {
 | 
				
			||||||
 | 
									t.Errorf("unexpected stderr:\n%s", cmp.Diff(test.expectStderr, sawError))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user