1
16
17 package integration
18
19 import (
20 "context"
21 "path"
22 "reflect"
23 "strings"
24 "testing"
25 "time"
26
27 "github.com/google/go-cmp/cmp"
28 "go.etcd.io/etcd/client/pkg/v3/transport"
29 clientv3 "go.etcd.io/etcd/client/v3"
30 "google.golang.org/grpc"
31 "sigs.k8s.io/yaml"
32
33 "k8s.io/apimachinery/pkg/api/errors"
34 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
35 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
36 "k8s.io/apimachinery/pkg/runtime/schema"
37 "k8s.io/apimachinery/pkg/util/json"
38 genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
39 "k8s.io/client-go/dynamic"
40 "k8s.io/utils/pointer"
41
42 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
43 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
44 serveroptions "k8s.io/apiextensions-apiserver/pkg/cmd/server/options"
45 "k8s.io/apiextensions-apiserver/test/integration/fixtures"
46 )
47
48 func TestPostInvalidObjectMeta(t *testing.T) {
49 tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
50 if err != nil {
51 t.Fatal(err)
52 }
53 defer tearDown()
54
55 noxuDefinition := fixtures.NewNoxuV1CustomResourceDefinition(apiextensionsv1.NamespaceScoped)
56 noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
57 if err != nil {
58 t.Fatal(err)
59 }
60
61 noxuResourceClient := newNamespacedCustomResourceClient("default", dynamicClient, noxuDefinition)
62
63 obj := fixtures.NewNoxuInstance("default", "foo")
64 unstructured.SetNestedField(obj.UnstructuredContent(), int64(42), "metadata", "unknown")
65 unstructured.SetNestedField(obj.UnstructuredContent(), nil, "metadata", "generation")
66 unstructured.SetNestedField(obj.UnstructuredContent(), map[string]interface{}{"foo": int64(42), "bar": "abc"}, "metadata", "labels")
67 _, err = instantiateCustomResource(t, obj, noxuResourceClient, noxuDefinition)
68 if err == nil {
69 t.Fatalf("unexpected non-error, expected invalid labels to be rejected: %v", err)
70 }
71 if status, ok := err.(errors.APIStatus); !ok {
72 t.Fatalf("expected APIStatus error, but got: %#v", err)
73 } else if !errors.IsBadRequest(err) {
74 t.Fatalf("expected BadRequst error, but got: %v", errors.ReasonForError(err))
75 } else if !strings.Contains(status.Status().Message, "cannot be handled") {
76 t.Fatalf("expected 'cannot be handled' error message, got: %v", status.Status().Message)
77 }
78
79 unstructured.SetNestedField(obj.UnstructuredContent(), map[string]interface{}{"bar": "abc"}, "metadata", "labels")
80 obj, err = instantiateCustomResource(t, obj, noxuResourceClient, noxuDefinition)
81 if err != nil {
82 t.Fatalf("unexpected error: %v", err)
83 }
84
85 if unknown, found, err := unstructured.NestedInt64(obj.UnstructuredContent(), "metadata", "unknown"); err != nil {
86 t.Errorf("unexpected error getting metadata.unknown: %v", err)
87 } else if found {
88 t.Errorf("unexpected metadata.unknown=%#v: expected this to be pruned", unknown)
89 }
90
91 if generation, found, err := unstructured.NestedInt64(obj.UnstructuredContent(), "metadata", "generation"); err != nil {
92 t.Errorf("unexpected error getting metadata.generation: %v", err)
93 } else if !found {
94 t.Errorf("expected metadata.generation=1: got: %d", generation)
95 } else if generation != 1 {
96 t.Errorf("unexpected metadata.generation=%d: expected this to be set to 1", generation)
97 }
98 }
99
100 func TestInvalidObjectMetaInStorage(t *testing.T) {
101 tearDown, config, options, err := fixtures.StartDefaultServer(t)
102 if err != nil {
103 t.Fatal(err)
104 }
105 defer tearDown()
106
107 apiExtensionClient, err := clientset.NewForConfig(config)
108 if err != nil {
109 t.Fatal(err)
110 }
111
112 dynamicClient, err := dynamic.NewForConfig(config)
113 if err != nil {
114 t.Fatal(err)
115 }
116
117 noxuDefinition := fixtures.NewNoxuV1CustomResourceDefinition(apiextensionsv1.NamespaceScoped)
118 noxuDefinition.Spec.Versions[0].Schema = &apiextensionsv1.CustomResourceValidation{
119 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
120 Type: "object",
121 Properties: map[string]apiextensionsv1.JSONSchemaProps{
122 "embedded": {
123 Type: "object",
124 XEmbeddedResource: true,
125 XPreserveUnknownFields: pointer.BoolPtr(true),
126 },
127 },
128 },
129 }
130 noxuDefinition, err = fixtures.CreateNewV1CustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
131 if err != nil {
132 t.Fatal(err)
133 }
134
135 RESTOptionsGetter := serveroptions.NewCRDRESTOptionsGetter(*options.RecommendedOptions.Etcd, nil, nil)
136 restOptions, err := RESTOptionsGetter.GetRESTOptions(schema.GroupResource{Group: noxuDefinition.Spec.Group, Resource: noxuDefinition.Spec.Names.Plural})
137 if err != nil {
138 t.Fatal(err)
139 }
140 tlsInfo := transport.TLSInfo{
141 CertFile: restOptions.StorageConfig.Transport.CertFile,
142 KeyFile: restOptions.StorageConfig.Transport.KeyFile,
143 TrustedCAFile: restOptions.StorageConfig.Transport.TrustedCAFile,
144 }
145 tlsConfig, err := tlsInfo.ClientConfig()
146 if err != nil {
147 t.Fatal(err)
148 }
149 etcdConfig := clientv3.Config{
150 Endpoints: restOptions.StorageConfig.Transport.ServerList,
151 DialTimeout: 20 * time.Second,
152 DialOptions: []grpc.DialOption{
153 grpc.WithBlock(),
154 },
155 TLS: tlsConfig,
156 }
157 etcdclient, err := clientv3.New(etcdConfig)
158 if err != nil {
159 t.Fatal(err)
160 }
161
162 t.Logf("Creating object with wrongly typed annotations and non-validating labels manually in etcd")
163
164 original := fixtures.NewNoxuInstance("default", "foo")
165 unstructured.SetNestedField(original.UnstructuredContent(), int64(42), "metadata", "unknown")
166 unstructured.SetNestedField(original.UnstructuredContent(), nil, "metadata", "generation")
167
168 unstructured.SetNestedField(original.UnstructuredContent(), map[string]interface{}{"foo": int64(42), "bar": "abc"}, "metadata", "annotations")
169 unstructured.SetNestedField(original.UnstructuredContent(), map[string]interface{}{"invalid": "x y"}, "metadata", "labels")
170 unstructured.SetNestedField(original.UnstructuredContent(), int64(42), "embedded", "metadata", "unknown")
171 unstructured.SetNestedField(original.UnstructuredContent(), map[string]interface{}{"foo": int64(42), "bar": "abc"}, "embedded", "metadata", "annotations")
172 unstructured.SetNestedField(original.UnstructuredContent(), map[string]interface{}{"invalid": "x y"}, "embedded", "metadata", "labels")
173 unstructured.SetNestedField(original.UnstructuredContent(), "Foo", "embedded", "kind")
174 unstructured.SetNestedField(original.UnstructuredContent(), "foo/v1", "embedded", "apiVersion")
175
176 ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
177 key := path.Join("/", restOptions.StorageConfig.Prefix, noxuDefinition.Spec.Group, "noxus/default/foo")
178 val, _ := json.Marshal(original.UnstructuredContent())
179 if _, err := etcdclient.Put(ctx, key, string(val)); err != nil {
180 t.Fatalf("unexpected error: %v", err)
181 }
182
183 t.Logf("Checking that invalid objects can be deleted")
184 noxuResourceClient := newNamespacedCustomResourceClient("default", dynamicClient, noxuDefinition)
185 if err := noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{}); err != nil {
186 t.Fatalf("Unexpected delete error %v", err)
187 }
188 if _, err := etcdclient.Put(ctx, key, string(val)); err != nil {
189 t.Fatalf("unexpected error: %v", err)
190 }
191
192 t.Logf("Checking that ObjectMeta is pruned from unknown fields")
193 obj, err := noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{})
194 if err != nil {
195 t.Fatalf("Unexpected error: %v", err)
196 }
197 objJSON, _ := json.Marshal(obj.Object)
198 t.Logf("Got object: %v", string(objJSON))
199
200 if unknown, found, err := unstructured.NestedFieldNoCopy(obj.UnstructuredContent(), "metadata", "unknown"); err != nil {
201 t.Errorf("Unexpected error: %v", err)
202 } else if found {
203 t.Errorf("Unexpected to find metadata.unknown=%#v", unknown)
204 }
205 if unknown, found, err := unstructured.NestedFieldNoCopy(obj.UnstructuredContent(), "embedded", "metadata", "unknown"); err != nil {
206 t.Errorf("Unexpected error: %v", err)
207 } else if found {
208 t.Errorf("Unexpected to find embedded.metadata.unknown=%#v", unknown)
209 }
210
211 t.Logf("Checking that metadata.generation=1")
212
213 if generation, found, err := unstructured.NestedInt64(obj.UnstructuredContent(), "metadata", "generation"); err != nil {
214 t.Errorf("unexpected error getting metadata.generation: %v", err)
215 } else if !found {
216 t.Errorf("expected metadata.generation=1: got: %d", generation)
217 } else if generation != 1 {
218 t.Errorf("unexpected metadata.generation=%d: expected this to be set to 1", generation)
219 }
220
221 t.Logf("Checking that ObjectMeta is pruned from wrongly-typed annotations")
222
223 if annotations, found, err := unstructured.NestedStringMap(obj.UnstructuredContent(), "metadata", "annotations"); err != nil {
224 t.Errorf("Unexpected error: %v", err)
225 } else if found {
226 t.Errorf("Unexpected to find metadata.annotations: %#v", annotations)
227 }
228 if annotations, found, err := unstructured.NestedStringMap(obj.UnstructuredContent(), "embedded", "metadata", "annotations"); err != nil {
229 t.Errorf("Unexpected error: %v", err)
230 } else if found {
231 t.Errorf("Unexpected to find embedded.metadata.annotations: %#v", annotations)
232 }
233
234 t.Logf("Checking that ObjectMeta still has the non-validating labels")
235
236 if labels, found, err := unstructured.NestedStringMap(obj.UnstructuredContent(), "metadata", "labels"); err != nil {
237 t.Errorf("unexpected error: %v", err)
238 } else if !found {
239 t.Errorf("Expected to find metadata.labels, but didn't")
240 } else if expected := map[string]string{"invalid": "x y"}; !reflect.DeepEqual(labels, expected) {
241 t.Errorf("Expected metadata.labels to be %#v, got: %#v", expected, labels)
242 }
243 if labels, found, err := unstructured.NestedStringMap(obj.UnstructuredContent(), "embedded", "metadata", "labels"); err != nil {
244 t.Errorf("Unexpected error: %v", err)
245 } else if !found {
246 t.Errorf("Expected to find embedded.metadata.labels, but didn't")
247 } else if expected := map[string]string{"invalid": "x y"}; !reflect.DeepEqual(labels, expected) {
248 t.Errorf("Expected embedded.metadata.labels to be %#v, got: %#v", expected, labels)
249 }
250
251 t.Logf("Trying to fail on updating with invalid labels")
252 unstructured.SetNestedField(obj.Object, "changed", "metadata", "labels", "something")
253 if got, err := noxuResourceClient.Update(context.TODO(), obj, metav1.UpdateOptions{}); err == nil {
254 objJSON, _ := json.Marshal(obj.Object)
255 gotJSON, _ := json.Marshal(got.Object)
256 t.Fatalf("Expected update error, but didn't get one\nin: %s\nresponse: %v", string(objJSON), string(gotJSON))
257 }
258
259 t.Logf("Trying to fail on updating with invalid embedded label")
260 unstructured.SetNestedField(obj.Object, "fixed", "metadata", "labels", "invalid")
261 if got, err := noxuResourceClient.Update(context.TODO(), obj, metav1.UpdateOptions{}); err == nil {
262 objJSON, _ := json.Marshal(obj.Object)
263 gotJSON, _ := json.Marshal(got.Object)
264 t.Fatalf("Expected update error, but didn't get one\nin: %s\nresponse: %v", string(objJSON), string(gotJSON))
265 }
266
267 t.Logf("Fixed all labels and update should work")
268 unstructured.SetNestedField(obj.Object, "fixed", "embedded", "metadata", "labels", "invalid")
269 if _, err := noxuResourceClient.Update(context.TODO(), obj, metav1.UpdateOptions{}); err != nil {
270 t.Errorf("Unexpected update error with fixed labels: %v", err)
271 }
272
273 t.Logf("Trying to fail on updating with wrongly-typed embedded label")
274 unstructured.SetNestedField(obj.Object, int64(42), "embedded", "metadata", "labels", "invalid")
275 if got, err := noxuResourceClient.Update(context.TODO(), obj, metav1.UpdateOptions{}); err == nil {
276 objJSON, _ := json.Marshal(obj.Object)
277 gotJSON, _ := json.Marshal(got.Object)
278 t.Fatalf("Expected update error, but didn't get one\nin: %s\nresponse: %v", string(objJSON), string(gotJSON))
279 }
280 }
281
282 var embeddedResourceFixture = &apiextensionsv1.CustomResourceDefinition{
283 ObjectMeta: metav1.ObjectMeta{Name: "foos.tests.example.com"},
284 Spec: apiextensionsv1.CustomResourceDefinitionSpec{
285 Group: "tests.example.com",
286 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
287 {
288 Name: "v1beta1",
289 Storage: true,
290 Served: true,
291 Subresources: &apiextensionsv1.CustomResourceSubresources{
292 Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
293 },
294 },
295 },
296 Names: apiextensionsv1.CustomResourceDefinitionNames{
297 Plural: "foos",
298 Singular: "foo",
299 Kind: "Foo",
300 ListKind: "FooList",
301 },
302 Scope: apiextensionsv1.ClusterScoped,
303 PreserveUnknownFields: false,
304 },
305 }
306
307 const (
308 embeddedResourceSchema = `
309 type: object
310 properties:
311 embedded:
312 type: object
313 x-kubernetes-embedded-resource: true
314 x-kubernetes-preserve-unknown-fields: true
315 noEmbeddedObject:
316 type: object
317 x-kubernetes-preserve-unknown-fields: true
318 embeddedNested:
319 type: object
320 x-kubernetes-embedded-resource: true
321 x-kubernetes-preserve-unknown-fields: true
322 properties:
323 embedded:
324 type: object
325 x-kubernetes-embedded-resource: true
326 x-kubernetes-preserve-unknown-fields: true
327 defaults:
328 type: object
329 x-kubernetes-embedded-resource: true
330 x-kubernetes-preserve-unknown-fields: true
331 default:
332 apiVersion: v1
333 kind: Pod
334 labels:
335 foo: bar
336 `
337
338 embeddedResourceInstance = `
339 kind: Foo
340 apiVersion: tests.example.com/v1beta1
341 embedded:
342 apiVersion: foo/v1
343 kind: Foo
344 metadata:
345 name: foo
346 unspecified: bar
347 noEmbeddedObject:
348 apiVersion: foo/v1
349 kind: Foo
350 metadata:
351 name: foo
352 unspecified: bar
353 embeddedNested:
354 apiVersion: foo/v1
355 kind: Foo
356 metadata:
357 name: foo
358 unspecified: bar
359 embedded:
360 apiVersion: foo/v1
361 kind: Foo
362 metadata:
363 name: foo
364 unspecified: bar
365 `
366
367 expectedEmbeddedResourceInstance = `
368 kind: Foo
369 apiVersion: tests.example.com/v1beta1
370 embedded:
371 apiVersion: foo/v1
372 kind: Foo
373 metadata:
374 name: foo
375 noEmbeddedObject:
376 apiVersion: foo/v1
377 kind: Foo
378 metadata:
379 name: foo
380 unspecified: bar
381 embeddedNested:
382 apiVersion: foo/v1
383 kind: Foo
384 metadata:
385 name: foo
386 embedded:
387 apiVersion: foo/v1
388 kind: Foo
389 metadata:
390 name: foo
391 defaults:
392 apiVersion: v1
393 kind: Pod
394 labels:
395 foo: bar
396 `
397
398 wronglyTypedEmbeddedResourceInstance = `
399 kind: Foo
400 apiVersion: tests.example.com/v1beta1
401 embedded:
402 apiVersion: foo/v1
403 kind: Foo
404 metadata:
405 name: instance
406 namespace: 42
407 `
408
409 invalidEmbeddedResourceInstance = `
410 kind: Foo
411 apiVersion: tests.example.com/v1beta1
412 embedded:
413 apiVersion: foo/v1
414 kind: "%"
415 metadata:
416 name: ..
417 embeddedNested:
418 apiVersion: foo/v1
419 kind: "%"
420 metadata:
421 name: ..
422 embedded:
423 apiVersion: foo/v1
424 kind: "%"
425 metadata:
426 name: ..
427 invalidDefaults: {}
428 `
429 )
430
431 func TestEmbeddedResources(t *testing.T) {
432 tearDownFn, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t)
433 if err != nil {
434 t.Fatal(err)
435 }
436 defer tearDownFn()
437
438 crd := embeddedResourceFixture.DeepCopy()
439 crd.Spec.Versions[0].Schema = &apiextensionsv1.CustomResourceValidation{}
440 if err := yaml.Unmarshal([]byte(embeddedResourceSchema), &crd.Spec.Versions[0].Schema.OpenAPIV3Schema); err != nil {
441 t.Fatal(err)
442 }
443
444 crd, err = fixtures.CreateNewV1CustomResourceDefinition(crd, apiExtensionClient, dynamicClient)
445 if err != nil {
446 t.Fatal(err)
447 }
448
449 t.Logf("Creating CR and expect 'unspecified' fields to be pruned inside ObjectMetas")
450 fooClient := dynamicClient.Resource(schema.GroupVersionResource{Group: crd.Spec.Group, Version: crd.Spec.Versions[0].Name, Resource: crd.Spec.Names.Plural})
451 foo := &unstructured.Unstructured{}
452 if err := yaml.Unmarshal([]byte(embeddedResourceInstance), &foo.Object); err != nil {
453 t.Fatal(err)
454 }
455 unstructured.SetNestedField(foo.Object, "foo", "metadata", "name")
456 foo, err = fooClient.Create(context.TODO(), foo, metav1.CreateOptions{})
457 if err != nil {
458 t.Fatalf("Unable to create CR: %v", err)
459 }
460 t.Logf("CR created: %#v", foo.UnstructuredContent())
461
462 t.Logf("Checking that everything unknown inside ObjectMeta is gone")
463 delete(foo.Object, "metadata")
464 var expected map[string]interface{}
465 if err := yaml.Unmarshal([]byte(expectedEmbeddedResourceInstance), &expected); err != nil {
466 t.Fatal(err)
467 }
468 if !reflect.DeepEqual(expected, foo.Object) {
469 t.Errorf("unexpected diff: %s", cmp.Diff(expected, foo.Object))
470 }
471
472 t.Logf("Trying to create wrongly typed CR")
473 wronglyTyped := &unstructured.Unstructured{}
474 if err := yaml.Unmarshal([]byte(wronglyTypedEmbeddedResourceInstance), &wronglyTyped.Object); err != nil {
475 t.Fatal(err)
476 }
477 unstructured.SetNestedField(wronglyTyped.Object, "invalid", "metadata", "name")
478 _, err = fooClient.Create(context.TODO(), wronglyTyped, metav1.CreateOptions{})
479 if err == nil {
480 t.Fatal("Expected creation to fail, but didn't")
481 }
482 t.Logf("Creation of wrongly typed object failed with: %v", err)
483
484 for _, s := range []string{
485 `embedded.metadata: Invalid value`,
486 } {
487 if !strings.Contains(err.Error(), s) {
488 t.Errorf("missing error: %s", s)
489 }
490 }
491
492 t.Logf("Trying to create invalid CR")
493 invalid := &unstructured.Unstructured{}
494 if err := yaml.Unmarshal([]byte(invalidEmbeddedResourceInstance), &invalid.Object); err != nil {
495 t.Fatal(err)
496 }
497 unstructured.SetNestedField(invalid.Object, "invalid", "metadata", "name")
498 unstructured.SetNestedField(invalid.Object, "x y", "metadata", "labels", "foo")
499 _, err = fooClient.Create(context.TODO(), invalid, metav1.CreateOptions{})
500 if err == nil {
501 t.Fatal("Expected creation to fail, but didn't")
502 }
503 t.Logf("Creation of invalid object failed with: %v", err)
504
505 invalidErrors := []string{
506 `[metadata.labels: Invalid value: "x y"`,
507 ` embedded.kind: Invalid value: "%"`,
508 ` embedded.metadata.name: Invalid value: ".."`,
509 ` embeddedNested.kind: Invalid value: "%"`,
510 ` embeddedNested.metadata.name: Invalid value: ".."`,
511 ` embeddedNested.embedded.kind: Invalid value: "%"`,
512 ` embeddedNested.embedded.metadata.name: Invalid value: ".."`,
513 }
514 for _, s := range invalidErrors {
515 if !strings.Contains(err.Error(), s) {
516 t.Errorf("missing error: %s", s)
517 }
518 }
519
520 t.Logf("Creating a valid CR and then updating it with invalid values, expecting the same errors")
521 valid := &unstructured.Unstructured{}
522 if err := yaml.Unmarshal([]byte(embeddedResourceInstance), &valid.Object); err != nil {
523 t.Fatal(err)
524 }
525 unstructured.SetNestedField(valid.Object, "valid", "metadata", "name")
526 valid, err = fooClient.Create(context.TODO(), valid, metav1.CreateOptions{})
527 if err != nil {
528 t.Fatalf("Unable to create CR: %v", err)
529 }
530 for k, v := range invalid.Object {
531 if k == "metadata" {
532 continue
533 }
534 valid.Object[k] = v
535 }
536 unstructured.SetNestedField(valid.Object, "x y", "metadata", "labels", "foo")
537 if _, err = fooClient.Update(context.TODO(), valid, metav1.UpdateOptions{}); err == nil {
538 t.Fatal("Expected update error, but got none")
539 }
540 t.Logf("Update failed with: %v", err)
541 for _, s := range invalidErrors {
542 if !strings.Contains(err.Error(), s) {
543 t.Errorf("missing error: %s", s)
544 }
545 }
546 }
547
View as plain text