1/* 2Copyright 2018 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package integration 18 19import ( 20 "context" 21 "fmt" 22 "math" 23 "reflect" 24 "sort" 25 "strings" 26 "testing" 27 28 autoscaling "k8s.io/api/autoscaling/v1" 29 apierrors "k8s.io/apimachinery/pkg/api/errors" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 32 "k8s.io/apimachinery/pkg/runtime/schema" 33 "k8s.io/apimachinery/pkg/types" 34 "k8s.io/client-go/dynamic" 35 36 apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 37 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 38 "k8s.io/apiextensions-apiserver/test/integration/fixtures" 39) 40 41var labelSelectorPath = ".status.labelSelector" 42var anotherLabelSelectorPath = ".status.anotherLabelSelector" 43 44func NewNoxuSubresourcesCRDs(scope apiextensionsv1beta1.ResourceScope) []*apiextensionsv1beta1.CustomResourceDefinition { 45 return []*apiextensionsv1beta1.CustomResourceDefinition{ 46 // CRD that uses top-level subresources 47 { 48 ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"}, 49 Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ 50 Group: "mygroup.example.com", 51 Version: "v1beta1", 52 Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ 53 Plural: "noxus", 54 Singular: "nonenglishnoxu", 55 Kind: "WishIHadChosenNoxu", 56 ShortNames: []string{"foo", "bar", "abc", "def"}, 57 ListKind: "NoxuItemList", 58 }, 59 Scope: scope, 60 Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{ 61 { 62 Name: "v1beta1", 63 Served: true, 64 Storage: true, 65 }, 66 { 67 Name: "v1", 68 Served: true, 69 Storage: false, 70 }, 71 }, 72 Subresources: &apiextensionsv1beta1.CustomResourceSubresources{ 73 Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{}, 74 Scale: &apiextensionsv1beta1.CustomResourceSubresourceScale{ 75 SpecReplicasPath: ".spec.replicas", 76 StatusReplicasPath: ".status.replicas", 77 LabelSelectorPath: &labelSelectorPath, 78 }, 79 }, 80 }, 81 }, 82 // CRD that uses per-version subresources 83 { 84 ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"}, 85 Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{ 86 Group: "mygroup.example.com", 87 Version: "v1beta1", 88 Names: apiextensionsv1beta1.CustomResourceDefinitionNames{ 89 Plural: "noxus", 90 Singular: "nonenglishnoxu", 91 Kind: "WishIHadChosenNoxu", 92 ShortNames: []string{"foo", "bar", "abc", "def"}, 93 ListKind: "NoxuItemList", 94 }, 95 Scope: scope, 96 Versions: []apiextensionsv1beta1.CustomResourceDefinitionVersion{ 97 { 98 Name: "v1beta1", 99 Served: true, 100 Storage: true, 101 Subresources: &apiextensionsv1beta1.CustomResourceSubresources{ 102 Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{}, 103 Scale: &apiextensionsv1beta1.CustomResourceSubresourceScale{ 104 SpecReplicasPath: ".spec.replicas", 105 StatusReplicasPath: ".status.replicas", 106 LabelSelectorPath: &labelSelectorPath, 107 }, 108 }, 109 }, 110 { 111 Name: "v1", 112 Served: true, 113 Storage: false, 114 Subresources: &apiextensionsv1beta1.CustomResourceSubresources{ 115 Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{}, 116 Scale: &apiextensionsv1beta1.CustomResourceSubresourceScale{ 117 SpecReplicasPath: ".spec.replicas", 118 StatusReplicasPath: ".status.replicas", 119 LabelSelectorPath: &anotherLabelSelectorPath, 120 }, 121 }, 122 }, 123 }, 124 }, 125 }, 126 } 127} 128 129func NewNoxuSubresourceInstance(namespace, name, version string) *unstructured.Unstructured { 130 return &unstructured.Unstructured{ 131 Object: map[string]interface{}{ 132 "apiVersion": fmt.Sprintf("mygroup.example.com/%s", version), 133 "kind": "WishIHadChosenNoxu", 134 "metadata": map[string]interface{}{ 135 "namespace": namespace, 136 "name": name, 137 }, 138 "spec": map[string]interface{}{ 139 "num": int64(10), 140 "replicas": int64(3), 141 }, 142 "status": map[string]interface{}{ 143 "replicas": int64(7), 144 }, 145 }, 146 } 147} 148 149func TestStatusSubresource(t *testing.T) { 150 tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) 151 if err != nil { 152 t.Fatal(err) 153 } 154 defer tearDown() 155 156 noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped) 157 for _, noxuDefinition := range noxuDefinitions { 158 noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) 159 if err != nil { 160 t.Fatal(err) 161 } 162 163 ns := "not-the-default" 164 for _, v := range noxuDefinition.Spec.Versions { 165 noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name) 166 _, err = instantiateVersionedCustomResource(t, NewNoxuSubresourceInstance(ns, "foo", v.Name), noxuResourceClient, noxuDefinition, v.Name) 167 if err != nil { 168 t.Fatalf("unable to create noxu instance: %v", err) 169 } 170 gottenNoxuInstance, err := noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{}) 171 if err != nil { 172 t.Fatal(err) 173 } 174 // status should not be set after creation 175 if val, ok := gottenNoxuInstance.Object["status"]; ok { 176 t.Fatalf("status should not be set after creation, got %v", val) 177 } 178 179 // .status.num = 20 180 err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "status", "num") 181 if err != nil { 182 t.Fatalf("unexpected error: %v", err) 183 } 184 185 // .spec.num = 20 186 err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "spec", "num") 187 if err != nil { 188 t.Fatalf("unexpected error: %v", err) 189 } 190 191 // UpdateStatus should not update spec. 192 // Check that .spec.num = 10 and .status.num = 20 193 updatedStatusInstance, err := noxuResourceClient.UpdateStatus(context.TODO(), gottenNoxuInstance, metav1.UpdateOptions{}) 194 if err != nil { 195 t.Fatalf("unable to update status: %v", err) 196 } 197 198 specNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "spec", "num") 199 if !found || err != nil { 200 t.Fatalf("unable to get .spec.num") 201 } 202 if specNum != int64(10) { 203 t.Fatalf(".spec.num: expected: %v, got: %v", int64(10), specNum) 204 } 205 206 statusNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "status", "num") 207 if !found || err != nil { 208 t.Fatalf("unable to get .status.num") 209 } 210 if statusNum != int64(20) { 211 t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum) 212 } 213 214 gottenNoxuInstance, err = noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{}) 215 if err != nil { 216 t.Fatal(err) 217 } 218 219 // .status.num = 40 220 err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "status", "num") 221 if err != nil { 222 t.Fatalf("unexpected error: %v", err) 223 } 224 225 // .spec.num = 40 226 err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "spec", "num") 227 if err != nil { 228 t.Fatalf("unexpected error: %v", err) 229 } 230 231 // Update should not update status. 232 // Check that .spec.num = 40 and .status.num = 20 233 updatedInstance, err := noxuResourceClient.Update(context.TODO(), gottenNoxuInstance, metav1.UpdateOptions{}) 234 if err != nil { 235 t.Fatalf("unable to update instance: %v", err) 236 } 237 238 specNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "spec", "num") 239 if !found || err != nil { 240 t.Fatalf("unable to get .spec.num") 241 } 242 if specNum != int64(40) { 243 t.Fatalf(".spec.num: expected: %v, got: %v", int64(40), specNum) 244 } 245 246 statusNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "status", "num") 247 if !found || err != nil { 248 t.Fatalf("unable to get .status.num") 249 } 250 if statusNum != int64(20) { 251 t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum) 252 } 253 noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{}) 254 } 255 if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { 256 t.Fatal(err) 257 } 258 } 259} 260 261func TestScaleSubresource(t *testing.T) { 262 groupResource := schema.GroupResource{ 263 Group: "mygroup.example.com", 264 Resource: "noxus", 265 } 266 267 tearDown, config, _, err := fixtures.StartDefaultServer(t) 268 if err != nil { 269 t.Fatal(err) 270 } 271 defer tearDown() 272 273 apiExtensionClient, err := clientset.NewForConfig(config) 274 if err != nil { 275 t.Fatal(err) 276 } 277 dynamicClient, err := dynamic.NewForConfig(config) 278 if err != nil { 279 t.Fatal(err) 280 } 281 282 noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped) 283 for _, noxuDefinition := range noxuDefinitions { 284 for _, v := range noxuDefinition.Spec.Versions { 285 // Start with a new CRD, so that the object doesn't have resourceVersion 286 noxuDefinition := noxuDefinition.DeepCopy() 287 288 subresources, err := getSubresourcesForVersion(noxuDefinition, v.Name) 289 if err != nil { 290 t.Fatal(err) 291 } 292 // set invalid json path for specReplicasPath 293 subresources.Scale.SpecReplicasPath = "foo,bar" 294 _, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) 295 if err == nil { 296 t.Fatalf("unexpected non-error: specReplicasPath should be a valid json path under .spec") 297 } 298 299 subresources.Scale.SpecReplicasPath = ".spec.replicas" 300 noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) 301 if err != nil { 302 t.Fatal(err) 303 } 304 305 ns := "not-the-default" 306 noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name) 307 _, err = instantiateVersionedCustomResource(t, NewNoxuSubresourceInstance(ns, "foo", v.Name), noxuResourceClient, noxuDefinition, v.Name) 308 if err != nil { 309 t.Fatalf("unable to create noxu instance: %v", err) 310 } 311 312 scaleClient, err := fixtures.CreateNewVersionedScaleClient(noxuDefinition, config, v.Name) 313 if err != nil { 314 t.Fatal(err) 315 } 316 317 // set .status.labelSelector = bar 318 gottenNoxuInstance, err := noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{}) 319 if err != nil { 320 t.Fatal(err) 321 } 322 err = unstructured.SetNestedField(gottenNoxuInstance.Object, "bar", strings.Split((*subresources.Scale.LabelSelectorPath)[1:], ".")...) 323 if err != nil { 324 t.Fatalf("unexpected error: %v", err) 325 } 326 _, err = noxuResourceClient.UpdateStatus(context.TODO(), gottenNoxuInstance, metav1.UpdateOptions{}) 327 if err != nil { 328 t.Fatalf("unable to update status: %v", err) 329 } 330 331 // get the scale object 332 gottenScale, err := scaleClient.Scales("not-the-default").Get(context.TODO(), groupResource, "foo", metav1.GetOptions{}) 333 if err != nil { 334 t.Fatal(err) 335 } 336 if gottenScale.Spec.Replicas != 3 { 337 t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 3, gottenScale.Spec.Replicas) 338 } 339 if gottenScale.Status.Selector != "bar" { 340 t.Fatalf("Scale.Status.Selector: expected: %v, got: %v", "bar", gottenScale.Status.Selector) 341 } 342 343 // check self link 344 expectedSelfLink := fmt.Sprintf("/apis/mygroup.example.com/%s/namespaces/not-the-default/noxus/foo/scale", v.Name) 345 if gottenScale.GetSelfLink() != expectedSelfLink { 346 t.Fatalf("Scale.Metadata.SelfLink: expected: %v, got: %v", expectedSelfLink, gottenScale.GetSelfLink()) 347 } 348 349 // update the scale object 350 // check that spec is updated, but status is not 351 gottenScale.Spec.Replicas = 5 352 gottenScale.Status.Selector = "baz" 353 updatedScale, err := scaleClient.Scales("not-the-default").Update(context.TODO(), groupResource, gottenScale, metav1.UpdateOptions{}) 354 if err != nil { 355 t.Fatal(err) 356 } 357 if updatedScale.Spec.Replicas != 5 { 358 t.Fatalf("replicas: expected: %v, got: %v", 5, updatedScale.Spec.Replicas) 359 } 360 if updatedScale.Status.Selector != "bar" { 361 t.Fatalf("scale should not update status: expected %v, got: %v", "bar", updatedScale.Status.Selector) 362 } 363 364 // check that .spec.replicas = 5, but status is not updated 365 updatedNoxuInstance, err := noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{}) 366 if err != nil { 367 t.Fatal(err) 368 } 369 specReplicas, found, err := unstructured.NestedInt64(updatedNoxuInstance.Object, "spec", "replicas") 370 if !found || err != nil { 371 t.Fatalf("unable to get .spec.replicas") 372 } 373 if specReplicas != 5 { 374 t.Fatalf("replicas: expected: %v, got: %v", 5, specReplicas) 375 } 376 statusLabelSelector, found, err := unstructured.NestedString(updatedNoxuInstance.Object, strings.Split((*subresources.Scale.LabelSelectorPath)[1:], ".")...) 377 if !found || err != nil { 378 t.Fatalf("unable to get %s", *subresources.Scale.LabelSelectorPath) 379 } 380 if statusLabelSelector != "bar" { 381 t.Fatalf("scale should not update status: expected %v, got: %v", "bar", statusLabelSelector) 382 } 383 384 // validate maximum value 385 // set .spec.replicas = math.MaxInt64 386 gottenNoxuInstance, err = noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{}) 387 if err != nil { 388 t.Fatal(err) 389 } 390 err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(math.MaxInt64), "spec", "replicas") 391 if err != nil { 392 t.Fatalf("unexpected error: %v", err) 393 } 394 _, err = noxuResourceClient.Update(context.TODO(), gottenNoxuInstance, metav1.UpdateOptions{}) 395 if err == nil { 396 t.Fatalf("unexpected non-error: .spec.replicas should be less than 2147483647") 397 } 398 noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{}) 399 if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { 400 t.Fatal(err) 401 } 402 } 403 } 404} 405 406func TestValidationSchemaWithStatus(t *testing.T) { 407 tearDown, config, _, err := fixtures.StartDefaultServer(t) 408 if err != nil { 409 t.Fatal(err) 410 } 411 defer tearDown() 412 413 apiExtensionClient, err := clientset.NewForConfig(config) 414 if err != nil { 415 t.Fatal(err) 416 } 417 dynamicClient, err := dynamic.NewForConfig(config) 418 if err != nil { 419 t.Fatal(err) 420 } 421 422 // fields other than properties in root schema are not allowed 423 noxuDefinition := newNoxuValidationCRDs(apiextensionsv1beta1.NamespaceScoped)[0] 424 noxuDefinition.Spec.Subresources = &apiextensionsv1beta1.CustomResourceSubresources{ 425 Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{}, 426 } 427 _, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) 428 if err == nil { 429 t.Fatalf(`unexpected non-error, expected: must not have "additionalProperties" at the root of the schema if the status subresource is enabled`) 430 } 431 432 // make sure we are not restricting fields to properties even in subschemas 433 noxuDefinition.Spec.Validation.OpenAPIV3Schema = &apiextensionsv1beta1.JSONSchemaProps{ 434 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 435 "spec": { 436 Description: "Validation for spec", 437 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 438 "replicas": { 439 Type: "integer", 440 }, 441 }, 442 }, 443 }, 444 Required: []string{"spec"}, 445 Description: "This is a description at the root of the schema", 446 } 447 _, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) 448 if err != nil { 449 t.Fatalf("unable to created crd %v: %v", noxuDefinition.Name, err) 450 } 451} 452 453func TestValidateOnlyStatus(t *testing.T) { 454 tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) 455 if err != nil { 456 t.Fatal(err) 457 } 458 defer tearDown() 459 460 // UpdateStatus should validate only status 461 // 1. create a crd with max value of .spec.num = 10 and .status.num = 10 462 // 2. create a cr with .spec.num = 10 and .status.num = 10 (valid) 463 // 3. update the spec of the cr with .spec.num = 15 (spec is invalid), expect no error 464 // 4. update the spec of the cr with .spec.num = 15 (spec is invalid), expect error 465 466 // max value of spec.num = 10 and status.num = 10 467 schema := &apiextensionsv1beta1.JSONSchemaProps{ 468 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 469 "spec": { 470 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 471 "num": { 472 Type: "integer", 473 Maximum: float64Ptr(10), 474 }, 475 }, 476 }, 477 "status": { 478 Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{ 479 "num": { 480 Type: "integer", 481 Maximum: float64Ptr(10), 482 }, 483 }, 484 }, 485 }, 486 } 487 488 noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped) 489 for i, noxuDefinition := range noxuDefinitions { 490 if i == 0 { 491 noxuDefinition.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{ 492 OpenAPIV3Schema: schema, 493 } 494 } else { 495 noxuDefinition.Spec.Versions[0].Schema = &apiextensionsv1beta1.CustomResourceValidation{ 496 OpenAPIV3Schema: schema, 497 } 498 schemaWithDescription := schema.DeepCopy() 499 schemaWithDescription.Description = "test" 500 noxuDefinition.Spec.Versions[1].Schema = &apiextensionsv1beta1.CustomResourceValidation{ 501 OpenAPIV3Schema: schemaWithDescription, 502 } 503 } 504 505 noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) 506 if err != nil { 507 t.Fatal(err) 508 } 509 ns := "not-the-default" 510 for _, v := range noxuDefinition.Spec.Versions { 511 noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name) 512 513 // set .spec.num = 10 and .status.num = 10 514 noxuInstance := NewNoxuSubresourceInstance(ns, "foo", v.Name) 515 err = unstructured.SetNestedField(noxuInstance.Object, int64(10), "status", "num") 516 if err != nil { 517 t.Fatalf("unexpected error: %v", err) 518 } 519 520 createdNoxuInstance, err := instantiateVersionedCustomResource(t, noxuInstance, noxuResourceClient, noxuDefinition, v.Name) 521 if err != nil { 522 t.Fatalf("unable to create noxu instance: %v", err) 523 } 524 525 // update the spec with .spec.num = 15, expecting no error 526 err = unstructured.SetNestedField(createdNoxuInstance.Object, int64(15), "spec", "num") 527 if err != nil { 528 t.Fatalf("unexpected error setting .spec.num: %v", err) 529 } 530 createdNoxuInstance, err = noxuResourceClient.UpdateStatus(context.TODO(), createdNoxuInstance, metav1.UpdateOptions{}) 531 if err != nil { 532 t.Fatalf("unexpected error: %v", err) 533 } 534 535 // update with .status.num = 15, expecting an error 536 err = unstructured.SetNestedField(createdNoxuInstance.Object, int64(15), "status", "num") 537 if err != nil { 538 t.Fatalf("unexpected error setting .status.num: %v", err) 539 } 540 _, err = noxuResourceClient.UpdateStatus(context.TODO(), createdNoxuInstance, metav1.UpdateOptions{}) 541 if err == nil { 542 t.Fatal("expected error, but got none") 543 } 544 statusError, isStatus := err.(*apierrors.StatusError) 545 if !isStatus || statusError == nil { 546 t.Fatalf("expected status error, got %T: %v", err, err) 547 } 548 if !strings.Contains(statusError.Error(), "Invalid value") { 549 t.Fatalf("expected 'Invalid value' in error, got: %v", err) 550 } 551 noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{}) 552 } 553 if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { 554 t.Fatal(err) 555 } 556 } 557} 558 559func TestSubresourcesDiscovery(t *testing.T) { 560 tearDown, config, _, err := fixtures.StartDefaultServer(t) 561 if err != nil { 562 t.Fatal(err) 563 } 564 defer tearDown() 565 566 apiExtensionClient, err := clientset.NewForConfig(config) 567 if err != nil { 568 t.Fatal(err) 569 } 570 dynamicClient, err := dynamic.NewForConfig(config) 571 if err != nil { 572 t.Fatal(err) 573 } 574 575 noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped) 576 for _, noxuDefinition := range noxuDefinitions { 577 noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) 578 if err != nil { 579 t.Fatal(err) 580 } 581 582 for _, v := range noxuDefinition.Spec.Versions { 583 group := "mygroup.example.com" 584 version := v.Name 585 586 resources, err := apiExtensionClient.Discovery().ServerResourcesForGroupVersion(group + "/" + version) 587 if err != nil { 588 t.Fatal(err) 589 } 590 591 if len(resources.APIResources) != 3 { 592 t.Fatalf("Expected exactly the resources \"noxus\", \"noxus/status\" and \"noxus/scale\" in group version %v/%v via discovery, got: %v", group, version, resources.APIResources) 593 } 594 595 // check discovery info for status 596 status := resources.APIResources[1] 597 598 if status.Name != "noxus/status" { 599 t.Fatalf("incorrect status via discovery: expected name: %v, got: %v", "noxus/status", status.Name) 600 } 601 602 if status.Namespaced != true { 603 t.Fatalf("incorrect status via discovery: expected namespace: %v, got: %v", true, status.Namespaced) 604 } 605 606 if status.Kind != "WishIHadChosenNoxu" { 607 t.Fatalf("incorrect status via discovery: expected kind: %v, got: %v", "WishIHadChosenNoxu", status.Kind) 608 } 609 610 expectedVerbs := []string{"get", "patch", "update"} 611 sort.Strings(status.Verbs) 612 if !reflect.DeepEqual([]string(status.Verbs), expectedVerbs) { 613 t.Fatalf("incorrect status via discovery: expected: %v, got: %v", expectedVerbs, status.Verbs) 614 } 615 616 // check discovery info for scale 617 scale := resources.APIResources[2] 618 619 if scale.Group != autoscaling.GroupName { 620 t.Fatalf("incorrect scale via discovery: expected group: %v, got: %v", autoscaling.GroupName, scale.Group) 621 } 622 623 if scale.Version != "v1" { 624 t.Fatalf("incorrect scale via discovery: expected version: %v, got %v", "v1", scale.Version) 625 } 626 627 if scale.Name != "noxus/scale" { 628 t.Fatalf("incorrect scale via discovery: expected name: %v, got: %v", "noxus/scale", scale.Name) 629 } 630 631 if scale.Namespaced != true { 632 t.Fatalf("incorrect scale via discovery: expected namespace: %v, got: %v", true, scale.Namespaced) 633 } 634 635 if scale.Kind != "Scale" { 636 t.Fatalf("incorrect scale via discovery: expected kind: %v, got: %v", "Scale", scale.Kind) 637 } 638 639 sort.Strings(scale.Verbs) 640 if !reflect.DeepEqual([]string(scale.Verbs), expectedVerbs) { 641 t.Fatalf("incorrect scale via discovery: expected: %v, got: %v", expectedVerbs, scale.Verbs) 642 } 643 } 644 if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { 645 t.Fatal(err) 646 } 647 } 648} 649 650func TestGeneration(t *testing.T) { 651 tearDown, apiExtensionClient, dynamicClient, err := fixtures.StartDefaultServerWithClients(t) 652 if err != nil { 653 t.Fatal(err) 654 } 655 defer tearDown() 656 657 noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped) 658 for _, noxuDefinition := range noxuDefinitions { 659 noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) 660 if err != nil { 661 t.Fatal(err) 662 } 663 664 ns := "not-the-default" 665 for _, v := range noxuDefinition.Spec.Versions { 666 noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name) 667 _, err = instantiateVersionedCustomResource(t, NewNoxuSubresourceInstance(ns, "foo", v.Name), noxuResourceClient, noxuDefinition, v.Name) 668 if err != nil { 669 t.Fatalf("unable to create noxu instance: %v", err) 670 } 671 672 // .metadata.generation = 1 673 gottenNoxuInstance, err := noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{}) 674 if err != nil { 675 t.Fatal(err) 676 } 677 if gottenNoxuInstance.GetGeneration() != 1 { 678 t.Fatalf(".metadata.generation should be 1 after creation") 679 } 680 681 // .status.num = 20 682 err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "status", "num") 683 if err != nil { 684 t.Fatalf("unexpected error: %v", err) 685 } 686 687 // UpdateStatus does not increment generation 688 updatedStatusInstance, err := noxuResourceClient.UpdateStatus(context.TODO(), gottenNoxuInstance, metav1.UpdateOptions{}) 689 if err != nil { 690 t.Fatalf("unable to update status: %v", err) 691 } 692 if updatedStatusInstance.GetGeneration() != 1 { 693 t.Fatalf("updating status should not increment .metadata.generation: expected: %v, got: %v", 1, updatedStatusInstance.GetGeneration()) 694 } 695 696 gottenNoxuInstance, err = noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{}) 697 if err != nil { 698 t.Fatal(err) 699 } 700 701 // .spec.num = 20 702 err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "spec", "num") 703 if err != nil { 704 t.Fatalf("unexpected error: %v", err) 705 } 706 707 // Update increments generation 708 updatedInstance, err := noxuResourceClient.Update(context.TODO(), gottenNoxuInstance, metav1.UpdateOptions{}) 709 if err != nil { 710 t.Fatalf("unable to update instance: %v", err) 711 } 712 if updatedInstance.GetGeneration() != 2 { 713 t.Fatalf("updating spec should increment .metadata.generation: expected: %v, got: %v", 2, updatedStatusInstance.GetGeneration()) 714 } 715 noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{}) 716 } 717 if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { 718 t.Fatal(err) 719 } 720 } 721} 722 723func TestSubresourcePatch(t *testing.T) { 724 groupResource := schema.GroupResource{ 725 Group: "mygroup.example.com", 726 Resource: "noxus", 727 } 728 729 tearDown, config, _, err := fixtures.StartDefaultServer(t) 730 if err != nil { 731 t.Fatal(err) 732 } 733 defer tearDown() 734 735 apiExtensionClient, err := clientset.NewForConfig(config) 736 if err != nil { 737 t.Fatal(err) 738 } 739 dynamicClient, err := dynamic.NewForConfig(config) 740 if err != nil { 741 t.Fatal(err) 742 } 743 744 noxuDefinitions := NewNoxuSubresourcesCRDs(apiextensionsv1beta1.NamespaceScoped) 745 for _, noxuDefinition := range noxuDefinitions { 746 noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) 747 if err != nil { 748 t.Fatal(err) 749 } 750 751 ns := "not-the-default" 752 for _, v := range noxuDefinition.Spec.Versions { 753 noxuResourceClient := newNamespacedCustomResourceVersionedClient(ns, dynamicClient, noxuDefinition, v.Name) 754 755 t.Logf("Creating foo") 756 _, err = instantiateVersionedCustomResource(t, NewNoxuSubresourceInstance(ns, "foo", v.Name), noxuResourceClient, noxuDefinition, v.Name) 757 if err != nil { 758 t.Fatalf("unable to create noxu instance: %v", err) 759 } 760 761 scaleClient, err := fixtures.CreateNewVersionedScaleClient(noxuDefinition, config, v.Name) 762 if err != nil { 763 t.Fatal(err) 764 } 765 766 t.Logf("Patching .status.num to 999") 767 patch := []byte(`{"spec": {"num":999}, "status": {"num":999}}`) 768 patchedNoxuInstance, err := noxuResourceClient.Patch(context.TODO(), "foo", types.MergePatchType, patch, metav1.PatchOptions{}, "status") 769 if err != nil { 770 t.Fatalf("unexpected error: %v", err) 771 } 772 773 expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num") // .status.num should be 999 774 expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num") // .spec.num should remain 10 775 rv, found, err := unstructured.NestedString(patchedNoxuInstance.UnstructuredContent(), "metadata", "resourceVersion") 776 if err != nil { 777 t.Fatal(err) 778 } 779 if !found { 780 t.Fatalf("metadata.resourceVersion not found") 781 } 782 783 // this call waits for the resourceVersion to be reached in the cache before returning. 784 // We need to do this because the patch gets its initial object from the storage, and the cache serves that. 785 // If it is out of date, then our initial patch is applied to an old resource version, which conflicts 786 // and then the updated object shows a conflicting diff, which permanently fails the patch. 787 // This gives expected stability in the patch without retrying on an known number of conflicts below in the test. 788 // See https://issue.k8s.io/42644 789 _, err = noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{ResourceVersion: patchedNoxuInstance.GetResourceVersion()}) 790 if err != nil { 791 t.Fatalf("unexpected error: %v", err) 792 } 793 794 // no-op patch 795 t.Logf("Patching .status.num again to 999") 796 patchedNoxuInstance, err = noxuResourceClient.Patch(context.TODO(), "foo", types.MergePatchType, patch, metav1.PatchOptions{}, "status") 797 if err != nil { 798 t.Fatalf("unexpected error: %v", err) 799 } 800 // make sure no-op patch does not increment resourceVersion 801 expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num") 802 expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num") 803 expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion") 804 805 // empty patch 806 t.Logf("Applying empty patch") 807 patchedNoxuInstance, err = noxuResourceClient.Patch(context.TODO(), "foo", types.MergePatchType, []byte(`{}`), metav1.PatchOptions{}, "status") 808 if err != nil { 809 t.Fatalf("unexpected error: %v", err) 810 } 811 812 // an empty patch is a no-op patch. make sure it does not increment resourceVersion 813 expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 999, "status", "num") 814 expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 10, "spec", "num") 815 expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion") 816 817 t.Logf("Patching .spec.replicas to 7") 818 patch = []byte(`{"spec": {"replicas":7}, "status": {"replicas":7}}`) 819 patchedNoxuInstance, err = noxuResourceClient.Patch(context.TODO(), "foo", types.MergePatchType, patch, metav1.PatchOptions{}, "scale") 820 if err != nil { 821 t.Fatalf("unexpected error: %v", err) 822 } 823 824 expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas") 825 expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas") // .status.replicas should remain 0 826 rv, found, err = unstructured.NestedString(patchedNoxuInstance.UnstructuredContent(), "metadata", "resourceVersion") 827 if err != nil { 828 t.Fatal(err) 829 } 830 if !found { 831 t.Fatalf("metadata.resourceVersion not found") 832 } 833 834 // this call waits for the resourceVersion to be reached in the cache before returning. 835 // We need to do this because the patch gets its initial object from the storage, and the cache serves that. 836 // If it is out of date, then our initial patch is applied to an old resource version, which conflicts 837 // and then the updated object shows a conflicting diff, which permanently fails the patch. 838 // This gives expected stability in the patch without retrying on an known number of conflicts below in the test. 839 // See https://issue.k8s.io/42644 840 _, err = noxuResourceClient.Get(context.TODO(), "foo", metav1.GetOptions{ResourceVersion: patchedNoxuInstance.GetResourceVersion()}) 841 if err != nil { 842 t.Fatalf("unexpected error: %v", err) 843 } 844 845 // Scale.Spec.Replicas = 7 but Scale.Status.Replicas should remain 0 846 gottenScale, err := scaleClient.Scales("not-the-default").Get(context.TODO(), groupResource, "foo", metav1.GetOptions{}) 847 if err != nil { 848 t.Fatal(err) 849 } 850 if gottenScale.Spec.Replicas != 7 { 851 t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 7, gottenScale.Spec.Replicas) 852 } 853 if gottenScale.Status.Replicas != 0 { 854 t.Fatalf("Scale.Status.Replicas: expected: %v, got: %v", 0, gottenScale.Spec.Replicas) 855 } 856 857 // no-op patch 858 t.Logf("Patching .spec.replicas again to 7") 859 patchedNoxuInstance, err = noxuResourceClient.Patch(context.TODO(), "foo", types.MergePatchType, patch, metav1.PatchOptions{}, "scale") 860 if err != nil { 861 t.Fatalf("unexpected error: %v", err) 862 } 863 // make sure no-op patch does not increment resourceVersion 864 expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas") 865 expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas") 866 expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion") 867 868 // empty patch 869 t.Logf("Applying empty patch") 870 patchedNoxuInstance, err = noxuResourceClient.Patch(context.TODO(), "foo", types.MergePatchType, []byte(`{}`), metav1.PatchOptions{}, "scale") 871 if err != nil { 872 t.Fatalf("unexpected error: %v", err) 873 } 874 // an empty patch is a no-op patch. make sure it does not increment resourceVersion 875 expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 7, "spec", "replicas") 876 expectInt64(t, patchedNoxuInstance.UnstructuredContent(), 0, "status", "replicas") 877 expectString(t, patchedNoxuInstance.UnstructuredContent(), rv, "metadata", "resourceVersion") 878 879 // make sure strategic merge patch is not supported for both status and scale 880 _, err = noxuResourceClient.Patch(context.TODO(), "foo", types.StrategicMergePatchType, patch, metav1.PatchOptions{}, "status") 881 if err == nil { 882 t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources") 883 } 884 885 _, err = noxuResourceClient.Patch(context.TODO(), "foo", types.StrategicMergePatchType, patch, metav1.PatchOptions{}, "scale") 886 if err == nil { 887 t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources") 888 } 889 noxuResourceClient.Delete(context.TODO(), "foo", metav1.DeleteOptions{}) 890 } 891 if err := fixtures.DeleteCustomResourceDefinition(noxuDefinition, apiExtensionClient); err != nil { 892 t.Fatal(err) 893 } 894 } 895} 896