1
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