...

Source file src/k8s.io/kubectl/pkg/cmd/create/create_token_test.go

Documentation: k8s.io/kubectl/pkg/cmd/create

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package create
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"io"
    23  	"net/http"
    24  	"os"
    25  	"reflect"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/google/go-cmp/cmp"
    30  	"k8s.io/utils/pointer"
    31  	kjson "sigs.k8s.io/json"
    32  
    33  	authenticationv1 "k8s.io/api/authentication/v1"
    34  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    35  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    36  	"k8s.io/apimachinery/pkg/runtime/schema"
    37  	"k8s.io/cli-runtime/pkg/genericiooptions"
    38  	"k8s.io/client-go/rest/fake"
    39  	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
    40  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    41  	"k8s.io/kubectl/pkg/scheme"
    42  )
    43  
    44  func TestCreateToken(t *testing.T) {
    45  	tests := []struct {
    46  		test string
    47  
    48  		name            string
    49  		namespace       string
    50  		output          string
    51  		boundObjectKind string
    52  		boundObjectName string
    53  		boundObjectUID  string
    54  		audiences       []string
    55  		duration        time.Duration
    56  
    57  		enableNodeBindingFeature bool
    58  
    59  		serverResponseToken string
    60  		serverResponseError string
    61  
    62  		expectRequestPath  string
    63  		expectTokenRequest *authenticationv1.TokenRequest
    64  
    65  		expectStdout string
    66  		expectStderr string
    67  	}{
    68  		{
    69  			test: "simple",
    70  			name: "mysa",
    71  
    72  			expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
    73  			expectTokenRequest: &authenticationv1.TokenRequest{
    74  				TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
    75  			},
    76  			serverResponseToken: "abc",
    77  			expectStdout:        "abc",
    78  		},
    79  
    80  		{
    81  			test:      "custom namespace",
    82  			name:      "custom-sa",
    83  			namespace: "custom-ns",
    84  
    85  			expectRequestPath: "/api/v1/namespaces/custom-ns/serviceaccounts/custom-sa/token",
    86  			expectTokenRequest: &authenticationv1.TokenRequest{
    87  				TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
    88  			},
    89  			serverResponseToken: "abc",
    90  			expectStdout:        "abc",
    91  		},
    92  
    93  		{
    94  			test:   "yaml",
    95  			name:   "mysa",
    96  			output: "yaml",
    97  
    98  			expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
    99  			expectTokenRequest: &authenticationv1.TokenRequest{
   100  				TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
   101  			},
   102  			serverResponseToken: "abc",
   103  			expectStdout: `apiVersion: authentication.k8s.io/v1
   104  kind: TokenRequest
   105  metadata:
   106    creationTimestamp: null
   107  spec:
   108    audiences: null
   109    boundObjectRef: null
   110    expirationSeconds: null
   111  status:
   112    expirationTimestamp: null
   113    token: abc
   114  `,
   115  		},
   116  
   117  		{
   118  			test:            "bad bound object kind",
   119  			name:            "mysa",
   120  			boundObjectKind: "Foo",
   121  			expectStderr:    `error: supported --bound-object-kind values are Pod, Secret`,
   122  		},
   123  		{
   124  			test:                     "bad bound object kind (node feature enabled)",
   125  			name:                     "mysa",
   126  			enableNodeBindingFeature: true,
   127  			boundObjectKind:          "Foo",
   128  			expectStderr:             `error: supported --bound-object-kind values are Node, Pod, Secret`,
   129  		},
   130  		{
   131  			test:            "missing bound object name",
   132  			name:            "mysa",
   133  			boundObjectKind: "Pod",
   134  			expectStderr:    `error: --bound-object-name is required if --bound-object-kind is provided`,
   135  		},
   136  		{
   137  			test:            "invalid bound object name",
   138  			name:            "mysa",
   139  			boundObjectName: "mypod",
   140  			expectStderr:    `error: --bound-object-name can only be set if --bound-object-kind is provided`,
   141  		},
   142  		{
   143  			test:           "invalid bound object uid",
   144  			name:           "mysa",
   145  			boundObjectUID: "myuid",
   146  			expectStderr:   `error: --bound-object-uid can only be set if --bound-object-kind is provided`,
   147  		},
   148  		{
   149  			test: "valid bound object",
   150  			name: "mysa",
   151  
   152  			boundObjectKind: "Pod",
   153  			boundObjectName: "mypod",
   154  			boundObjectUID:  "myuid",
   155  
   156  			expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
   157  			expectTokenRequest: &authenticationv1.TokenRequest{
   158  				TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
   159  				Spec: authenticationv1.TokenRequestSpec{
   160  					BoundObjectRef: &authenticationv1.BoundObjectReference{
   161  						Kind:       "Pod",
   162  						APIVersion: "v1",
   163  						Name:       "mypod",
   164  						UID:        "myuid",
   165  					},
   166  				},
   167  			},
   168  			serverResponseToken: "abc",
   169  			expectStdout:        "abc",
   170  		},
   171  		{
   172  			test: "valid bound object (Node)",
   173  			name: "mysa",
   174  
   175  			enableNodeBindingFeature: true,
   176  			boundObjectKind:          "Node",
   177  			boundObjectName:          "mynode",
   178  			boundObjectUID:           "myuid",
   179  
   180  			expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
   181  			expectTokenRequest: &authenticationv1.TokenRequest{
   182  				TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
   183  				Spec: authenticationv1.TokenRequestSpec{
   184  					BoundObjectRef: &authenticationv1.BoundObjectReference{
   185  						Kind:       "Node",
   186  						APIVersion: "v1",
   187  						Name:       "mynode",
   188  						UID:        "myuid",
   189  					},
   190  				},
   191  			},
   192  			serverResponseToken: "abc",
   193  			expectStdout:        "abc",
   194  		},
   195  		{
   196  			test:         "invalid audience",
   197  			name:         "mysa",
   198  			audiences:    []string{"test", "", "test2"},
   199  			expectStderr: `error: --audience must not be an empty string`,
   200  		},
   201  		{
   202  			test: "valid audiences",
   203  			name: "mysa",
   204  
   205  			audiences: []string{"test,value1", "test,value2"},
   206  
   207  			expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
   208  			expectTokenRequest: &authenticationv1.TokenRequest{
   209  				TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
   210  				Spec: authenticationv1.TokenRequestSpec{
   211  					Audiences: []string{"test,value1", "test,value2"},
   212  				},
   213  			},
   214  			serverResponseToken: "abc",
   215  			expectStdout:        "abc",
   216  		},
   217  
   218  		{
   219  			test:         "invalid duration",
   220  			name:         "mysa",
   221  			duration:     -1,
   222  			expectStderr: `error: --duration must be greater than or equal to 0`,
   223  		},
   224  		{
   225  			test:         "invalid duration unit",
   226  			name:         "mysa",
   227  			duration:     time.Microsecond,
   228  			expectStderr: `error: --duration cannot be expressed in units less than seconds`,
   229  		},
   230  		{
   231  			test: "valid duration",
   232  			name: "mysa",
   233  
   234  			duration: 1000 * time.Second,
   235  
   236  			expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
   237  			expectTokenRequest: &authenticationv1.TokenRequest{
   238  				TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
   239  				Spec: authenticationv1.TokenRequestSpec{
   240  					ExpirationSeconds: pointer.Int64(1000),
   241  				},
   242  			},
   243  			serverResponseToken: "abc",
   244  			expectStdout:        "abc",
   245  		},
   246  		{
   247  			test: "zero duration act as default",
   248  			name: "mysa",
   249  
   250  			duration: 0 * time.Second,
   251  
   252  			expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
   253  			expectTokenRequest: &authenticationv1.TokenRequest{
   254  				TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
   255  				Spec: authenticationv1.TokenRequestSpec{
   256  					ExpirationSeconds: nil,
   257  				},
   258  			},
   259  			serverResponseToken: "abc",
   260  			expectStdout:        "abc",
   261  		},
   262  		{
   263  			test: "server error",
   264  			name: "mysa",
   265  
   266  			expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
   267  			expectTokenRequest: &authenticationv1.TokenRequest{
   268  				TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
   269  			},
   270  			serverResponseError: "bad bad request",
   271  			expectStderr:        `error: failed to create token:  "bad bad request" is invalid`,
   272  		},
   273  		{
   274  			test: "server missing token",
   275  			name: "mysa",
   276  
   277  			expectRequestPath: "/api/v1/namespaces/test/serviceaccounts/mysa/token",
   278  			expectTokenRequest: &authenticationv1.TokenRequest{
   279  				TypeMeta: metav1.TypeMeta{APIVersion: "authentication.k8s.io/v1", Kind: "TokenRequest"},
   280  			},
   281  			serverResponseToken: "",
   282  			expectStderr:        `error: failed to create token: no token in server response`,
   283  		},
   284  	}
   285  
   286  	for _, test := range tests {
   287  		t.Run(test.test, func(t *testing.T) {
   288  			defer cmdutil.DefaultBehaviorOnFatal()
   289  			sawError := ""
   290  			cmdutil.BehaviorOnFatal(func(str string, code int) {
   291  				sawError = str
   292  			})
   293  
   294  			namespace := "test"
   295  			if test.namespace != "" {
   296  				namespace = test.namespace
   297  			}
   298  			tf := cmdtesting.NewTestFactory().WithNamespace(namespace)
   299  			defer tf.Cleanup()
   300  
   301  			tf.Client = &fake.RESTClient{}
   302  
   303  			var code int
   304  			var body []byte
   305  			if len(test.serverResponseError) > 0 {
   306  				code = 422
   307  				response := apierrors.NewInvalid(schema.GroupKind{Group: "", Kind: ""}, test.serverResponseError, nil)
   308  				response.ErrStatus.APIVersion = "v1"
   309  				response.ErrStatus.Kind = "Status"
   310  				body, _ = json.Marshal(response.ErrStatus)
   311  			} else {
   312  				code = 200
   313  				response := authenticationv1.TokenRequest{
   314  					TypeMeta: metav1.TypeMeta{
   315  						APIVersion: "authentication.k8s.io/v1",
   316  						Kind:       "TokenRequest",
   317  					},
   318  					Status: authenticationv1.TokenRequestStatus{Token: test.serverResponseToken},
   319  				}
   320  				body, _ = json.Marshal(response)
   321  			}
   322  
   323  			ns := scheme.Codecs.WithoutConversion()
   324  			var tokenRequest *authenticationv1.TokenRequest
   325  			tf.Client = &fake.RESTClient{
   326  				NegotiatedSerializer: ns,
   327  				Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   328  					if req.URL.Path != test.expectRequestPath {
   329  						t.Fatalf("expected %q, got %q", test.expectRequestPath, req.URL.Path)
   330  					}
   331  					data, err := io.ReadAll(req.Body)
   332  					if err != nil {
   333  						t.Fatal(err)
   334  					}
   335  					tokenRequest = &authenticationv1.TokenRequest{}
   336  					if strictErrs, err := kjson.UnmarshalStrict(data, tokenRequest); err != nil {
   337  						t.Fatal(err)
   338  					} else if len(strictErrs) > 0 {
   339  						t.Fatal(strictErrs)
   340  					}
   341  
   342  					return &http.Response{
   343  						StatusCode: code,
   344  						Body:       io.NopCloser(bytes.NewBuffer(body)),
   345  					}, nil
   346  				}),
   347  			}
   348  			tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
   349  
   350  			ioStreams, _, stdout, _ := genericiooptions.NewTestIOStreams()
   351  			cmd := NewCmdCreateToken(tf, ioStreams)
   352  			if test.output != "" {
   353  				cmd.Flags().Set("output", test.output)
   354  			}
   355  			if test.boundObjectKind != "" {
   356  				cmd.Flags().Set("bound-object-kind", test.boundObjectKind)
   357  			}
   358  			if test.boundObjectName != "" {
   359  				cmd.Flags().Set("bound-object-name", test.boundObjectName)
   360  			}
   361  			if test.boundObjectUID != "" {
   362  				cmd.Flags().Set("bound-object-uid", test.boundObjectUID)
   363  			}
   364  			for _, aud := range test.audiences {
   365  				cmd.Flags().Set("audience", aud)
   366  			}
   367  			if test.duration != 0 {
   368  				cmd.Flags().Set("duration", test.duration.String())
   369  			}
   370  			if test.enableNodeBindingFeature {
   371  				os.Setenv("KUBECTL_NODE_BOUND_TOKENS", "true")
   372  				defer os.Unsetenv("KUBECTL_NODE_BOUND_TOKENS")
   373  			}
   374  			cmd.Run(cmd, []string{test.name})
   375  
   376  			if !reflect.DeepEqual(tokenRequest, test.expectTokenRequest) {
   377  				t.Fatalf("unexpected request:\n%s", cmp.Diff(test.expectTokenRequest, tokenRequest))
   378  			}
   379  
   380  			if stdout.String() != test.expectStdout {
   381  				t.Errorf("unexpected stdout:\n%s", cmp.Diff(test.expectStdout, stdout.String()))
   382  			}
   383  			if sawError != test.expectStderr {
   384  				t.Errorf("unexpected stderr:\n%s", cmp.Diff(test.expectStderr, sawError))
   385  			}
   386  		})
   387  	}
   388  }
   389  

View as plain text