k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/e2e/apimachinery/crd_publish_openapi.go (about) 1 /* 2 Copyright 2019 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 apimachinery 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "io" 24 "net/http" 25 "regexp" 26 "strings" 27 "time" 28 29 "github.com/onsi/ginkgo/v2" 30 "sigs.k8s.io/yaml" 31 32 openapiutil "k8s.io/kube-openapi/pkg/util" 33 "k8s.io/utils/pointer" 34 35 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" 36 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 37 "k8s.io/apiextensions-apiserver/pkg/apiserver/validation" 38 apiequality "k8s.io/apimachinery/pkg/api/equality" 39 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 40 "k8s.io/apimachinery/pkg/types" 41 "k8s.io/apimachinery/pkg/util/wait" 42 k8sclientset "k8s.io/client-go/kubernetes" 43 "k8s.io/client-go/rest" 44 "k8s.io/kube-openapi/pkg/validation/spec" 45 "k8s.io/kubernetes/test/e2e/framework" 46 e2ekubectl "k8s.io/kubernetes/test/e2e/framework/kubectl" 47 "k8s.io/kubernetes/test/utils/crd" 48 admissionapi "k8s.io/pod-security-admission/api" 49 ) 50 51 var ( 52 metaPattern = `"kind":"%s","apiVersion":"%s/%s","metadata":{"name":"%s"}` 53 ) 54 55 var _ = SIGDescribe("CustomResourcePublishOpenAPI [Privileged:ClusterAdmin]", func() { 56 f := framework.NewDefaultFramework("crd-publish-openapi") 57 f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged 58 59 /* 60 Release: v1.16 61 Testname: Custom Resource OpenAPI Publish, with validation schema 62 Description: Register a custom resource definition with a validating schema consisting of objects, arrays and 63 primitives. Attempt to create and apply a change a custom resource using valid properties, via kubectl; 64 kubectl validation MUST pass. Attempt both operations with unknown properties and without required 65 properties; kubectl validation MUST reject the operations. Attempt kubectl explain; the output MUST 66 explain the custom resource properties. Attempt kubectl explain on custom resource properties; the output MUST 67 explain the nested custom resource properties. 68 All validation should be the same. 69 */ 70 framework.ConformanceIt("works for CRD with validation schema", func(ctx context.Context) { 71 crd, err := setupCRD(f, schemaFoo, "foo", "v1") 72 if err != nil { 73 framework.Failf("%v", err) 74 } 75 76 meta := fmt.Sprintf(metaPattern, crd.Crd.Spec.Names.Kind, crd.Crd.Spec.Group, crd.Crd.Spec.Versions[0].Name, "test-foo") 77 ns := fmt.Sprintf("--namespace=%v", f.Namespace.Name) 78 79 ginkgo.By("kubectl validation (kubectl create and apply) allows request with known and required properties") 80 validCR := fmt.Sprintf(`{%s,"spec":{"bars":[{"name":"test-bar"}]}}`, meta) 81 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, validCR, ns, "create", "-f", "-"); err != nil { 82 framework.Failf("failed to create valid CR %s: %v", validCR, err) 83 } 84 if _, err := e2ekubectl.RunKubectl(f.Namespace.Name, ns, "delete", crd.Crd.Spec.Names.Plural, "test-foo"); err != nil { 85 framework.Failf("failed to delete valid CR: %v", err) 86 } 87 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, validCR, ns, "apply", "-f", "-"); err != nil { 88 framework.Failf("failed to apply valid CR %s: %v", validCR, err) 89 } 90 if _, err := e2ekubectl.RunKubectl(f.Namespace.Name, ns, "delete", crd.Crd.Spec.Names.Plural, "test-foo"); err != nil { 91 framework.Failf("failed to delete valid CR: %v", err) 92 } 93 94 ginkgo.By("kubectl validation (kubectl create and apply) rejects request with value outside defined enum values") 95 badEnumValueCR := fmt.Sprintf(`{%s,"spec":{"bars":[{"name":"test-bar", "feeling":"NonExistentValue"}]}}`, meta) 96 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, badEnumValueCR, ns, "create", "-f", "-"); err == nil || !strings.Contains(err.Error(), `Unsupported value: "NonExistentValue"`) { 97 framework.Failf("unexpected no error when creating CR with unknown enum value: %v", err) 98 } 99 100 // TODO: server-side validation and client-side validation produce slightly different error messages. 101 // Because server-side is default in beta but not GA yet, we will produce different behaviors in the default vs GA only conformance tests. We have made the error generic enough to pass both, but should go back and make the error more specific once server-side validation goes GA. 102 ginkgo.By("kubectl validation (kubectl create and apply) rejects request with unknown properties when disallowed by the schema") 103 unknownCR := fmt.Sprintf(`{%s,"spec":{"foo":true}}`, meta) 104 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, unknownCR, ns, "create", "-f", "-"); err == nil || (!strings.Contains(err.Error(), `unknown field "foo"`) && !strings.Contains(err.Error(), `unknown field "spec.foo"`)) { 105 framework.Failf("unexpected no error when creating CR with unknown field: %v", err) 106 } 107 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, unknownCR, ns, "apply", "-f", "-"); err == nil || (!strings.Contains(err.Error(), `unknown field "foo"`) && !strings.Contains(err.Error(), `unknown field "spec.foo"`)) { 108 framework.Failf("unexpected no error when applying CR with unknown field: %v", err) 109 } 110 111 // TODO: see above note, we should check the value of the error once server-side validation is GA. 112 ginkgo.By("kubectl validation (kubectl create and apply) rejects request without required properties") 113 noRequireCR := fmt.Sprintf(`{%s,"spec":{"bars":[{"age":"10"}]}}`, meta) 114 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, noRequireCR, ns, "create", "-f", "-"); err == nil || (!strings.Contains(err.Error(), `missing required field "name"`) && !strings.Contains(err.Error(), `spec.bars[0].name: Required value`)) { 115 framework.Failf("unexpected no error when creating CR without required field: %v", err) 116 } 117 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, noRequireCR, ns, "apply", "-f", "-"); err == nil || (!strings.Contains(err.Error(), `missing required field "name"`) && !strings.Contains(err.Error(), `spec.bars[0].name: Required value`)) { 118 framework.Failf("unexpected no error when applying CR without required field: %v", err) 119 } 120 121 ginkgo.By("kubectl explain works to explain CR properties") 122 if err := verifyKubectlExplain(f.Namespace.Name, crd.Crd.Spec.Names.Plural, `(?s)DESCRIPTION:.*Foo CRD for Testing.*FIELDS:.*apiVersion.*<string>.*APIVersion defines.*spec.*<Object>.*Specification of Foo`); err != nil { 123 framework.Failf("%v", err) 124 } 125 126 ginkgo.By("kubectl explain works to explain CR properties recursively") 127 if err := verifyKubectlExplain(f.Namespace.Name, crd.Crd.Spec.Names.Plural+".metadata", `(?s)DESCRIPTION:.*Standard object's metadata.*FIELDS:.*creationTimestamp.*<string>.*CreationTimestamp is a timestamp`); err != nil { 128 framework.Failf("%v", err) 129 } 130 if err := verifyKubectlExplain(f.Namespace.Name, crd.Crd.Spec.Names.Plural+".spec", `(?s)DESCRIPTION:.*Specification of Foo.*FIELDS:.*bars.*<\[\]Object>.*List of Bars and their specs`); err != nil { 131 framework.Failf("%v", err) 132 } 133 if err := verifyKubectlExplain(f.Namespace.Name, crd.Crd.Spec.Names.Plural+".spec.bars", `(?s)(FIELD|RESOURCE):.*bars.*<\[\]Object>.*DESCRIPTION:.*List of Bars and their specs.*FIELDS:.*bazs.*<\[\]string>.*List of Bazs.*name.*<string>.*Name of Bar`); err != nil { 134 framework.Failf("%v", err) 135 } 136 137 ginkgo.By("kubectl explain works to return error when explain is called on property that doesn't exist") 138 if _, err := e2ekubectl.RunKubectl(f.Namespace.Name, "explain", crd.Crd.Spec.Names.Plural+".spec.bars2"); err == nil || !strings.Contains(err.Error(), `field "bars2" does not exist`) { 139 framework.Failf("unexpected no error when explaining property that doesn't exist: %v", err) 140 } 141 142 if err := cleanupCRD(ctx, f, crd); err != nil { 143 framework.Failf("%v", err) 144 } 145 }) 146 147 /* 148 Release: v1.16 149 Testname: Custom Resource OpenAPI Publish, with x-kubernetes-preserve-unknown-fields in object 150 Description: Register a custom resource definition with x-kubernetes-preserve-unknown-fields in the top level object. 151 Attempt to create and apply a change a custom resource, via kubectl; kubectl validation MUST accept unknown 152 properties. Attempt kubectl explain; the output MUST contain a valid DESCRIPTION stanza. 153 */ 154 framework.ConformanceIt("works for CRD without validation schema", func(ctx context.Context) { 155 crd, err := setupCRD(f, nil, "empty", "v1") 156 if err != nil { 157 framework.Failf("%v", err) 158 } 159 160 meta := fmt.Sprintf(metaPattern, crd.Crd.Spec.Names.Kind, crd.Crd.Spec.Group, crd.Crd.Spec.Versions[0].Name, "test-cr") 161 ns := fmt.Sprintf("--namespace=%v", f.Namespace.Name) 162 163 ginkgo.By("kubectl validation (kubectl create and apply) allows request with any unknown properties") 164 randomCR := fmt.Sprintf(`{%s,"a":{"b":[{"c":"d"}]}}`, meta) 165 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, randomCR, ns, "create", "-f", "-"); err != nil { 166 framework.Failf("failed to create random CR %s for CRD without schema: %v", randomCR, err) 167 } 168 if _, err := e2ekubectl.RunKubectl(f.Namespace.Name, ns, "delete", crd.Crd.Spec.Names.Plural, "test-cr"); err != nil { 169 framework.Failf("failed to delete random CR: %v", err) 170 } 171 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, randomCR, ns, "apply", "-f", "-"); err != nil { 172 framework.Failf("failed to apply random CR %s for CRD without schema: %v", randomCR, err) 173 } 174 if _, err := e2ekubectl.RunKubectl(f.Namespace.Name, ns, "delete", crd.Crd.Spec.Names.Plural, "test-cr"); err != nil { 175 framework.Failf("failed to delete random CR: %v", err) 176 } 177 178 ginkgo.By("kubectl explain works to explain CR without validation schema") 179 if err := verifyKubectlExplain(f.Namespace.Name, crd.Crd.Spec.Names.Plural, `(?s)DESCRIPTION:.*<empty>`); err != nil { 180 framework.Failf("%v", err) 181 } 182 183 if err := cleanupCRD(ctx, f, crd); err != nil { 184 framework.Failf("%v", err) 185 } 186 }) 187 188 /* 189 Release: v1.16 190 Testname: Custom Resource OpenAPI Publish, with x-kubernetes-preserve-unknown-fields at root 191 Description: Register a custom resource definition with x-kubernetes-preserve-unknown-fields in the schema root. 192 Attempt to create and apply a change a custom resource, via kubectl; kubectl validation MUST accept unknown 193 properties. Attempt kubectl explain; the output MUST show the custom resource KIND. 194 */ 195 framework.ConformanceIt("works for CRD preserving unknown fields at the schema root", func(ctx context.Context) { 196 crd, err := setupCRDAndVerifySchema(f, schemaPreserveRoot, nil, "unknown-at-root", "v1") 197 if err != nil { 198 framework.Failf("%v", err) 199 } 200 201 meta := fmt.Sprintf(metaPattern, crd.Crd.Spec.Names.Kind, crd.Crd.Spec.Group, crd.Crd.Spec.Versions[0].Name, "test-cr") 202 ns := fmt.Sprintf("--namespace=%v", f.Namespace.Name) 203 204 ginkgo.By("kubectl validation (kubectl create and apply) allows request with any unknown properties") 205 randomCR := fmt.Sprintf(`{%s,"a":{"b":[{"c":"d"}]}}`, meta) 206 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, randomCR, ns, "create", "-f", "-"); err != nil { 207 framework.Failf("failed to create random CR %s for CRD that allows unknown properties at the root: %v", randomCR, err) 208 } 209 if _, err := e2ekubectl.RunKubectl(f.Namespace.Name, ns, "delete", crd.Crd.Spec.Names.Plural, "test-cr"); err != nil { 210 framework.Failf("failed to delete random CR: %v", err) 211 } 212 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, randomCR, ns, "apply", "-f", "-"); err != nil { 213 framework.Failf("failed to apply random CR %s for CRD without schema: %v", randomCR, err) 214 } 215 if _, err := e2ekubectl.RunKubectl(f.Namespace.Name, ns, "delete", crd.Crd.Spec.Names.Plural, "test-cr"); err != nil { 216 framework.Failf("failed to delete random CR: %v", err) 217 } 218 219 ginkgo.By("kubectl explain works to explain CR") 220 if err := verifyKubectlExplain(f.Namespace.Name, crd.Crd.Spec.Names.Plural, fmt.Sprintf(`(?s)KIND:.*%s`, crd.Crd.Spec.Names.Kind)); err != nil { 221 framework.Failf("%v", err) 222 } 223 224 if err := cleanupCRD(ctx, f, crd); err != nil { 225 framework.Failf("%v", err) 226 } 227 }) 228 229 /* 230 Release: v1.16 231 Testname: Custom Resource OpenAPI Publish, with x-kubernetes-preserve-unknown-fields in embedded object 232 Description: Register a custom resource definition with x-kubernetes-preserve-unknown-fields in an embedded object. 233 Attempt to create and apply a change a custom resource, via kubectl; kubectl validation MUST accept unknown 234 properties. Attempt kubectl explain; the output MUST show that x-preserve-unknown-properties is used on the 235 nested field. 236 */ 237 framework.ConformanceIt("works for CRD preserving unknown fields in an embedded object", func(ctx context.Context) { 238 crd, err := setupCRDAndVerifySchema(f, schemaPreserveNested, nil, "unknown-in-nested", "v1") 239 if err != nil { 240 framework.Failf("%v", err) 241 } 242 243 meta := fmt.Sprintf(metaPattern, crd.Crd.Spec.Names.Kind, crd.Crd.Spec.Group, crd.Crd.Spec.Versions[0].Name, "test-cr") 244 ns := fmt.Sprintf("--namespace=%v", f.Namespace.Name) 245 246 ginkgo.By("kubectl validation (kubectl create and apply) allows request with any unknown properties") 247 randomCR := fmt.Sprintf(`{%s,"spec":{"a":null,"b":[{"c":"d"}]}}`, meta) 248 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, randomCR, ns, "create", "-f", "-"); err != nil { 249 framework.Failf("failed to create random CR %s for CRD that allows unknown properties in a nested object: %v", randomCR, err) 250 } 251 if _, err := e2ekubectl.RunKubectl(f.Namespace.Name, ns, "delete", crd.Crd.Spec.Names.Plural, "test-cr"); err != nil { 252 framework.Failf("failed to delete random CR: %v", err) 253 } 254 if _, err := e2ekubectl.RunKubectlInput(f.Namespace.Name, randomCR, ns, "apply", "-f", "-"); err != nil { 255 framework.Failf("failed to apply random CR %s for CRD without schema: %v", randomCR, err) 256 } 257 if _, err := e2ekubectl.RunKubectl(f.Namespace.Name, ns, "delete", crd.Crd.Spec.Names.Plural, "test-cr"); err != nil { 258 framework.Failf("failed to delete random CR: %v", err) 259 } 260 261 ginkgo.By("kubectl explain works to explain CR") 262 if err := verifyKubectlExplain(f.Namespace.Name, crd.Crd.Spec.Names.Plural, `(?s)DESCRIPTION:.*preserve-unknown-properties in nested field for Testing`); err != nil { 263 framework.Failf("%v", err) 264 } 265 266 if err := cleanupCRD(ctx, f, crd); err != nil { 267 framework.Failf("%v", err) 268 } 269 }) 270 271 /* 272 Release: v1.16 273 Testname: Custom Resource OpenAPI Publish, varying groups 274 Description: Register multiple custom resource definitions spanning different groups and versions; 275 OpenAPI definitions MUST be published for custom resource definitions. 276 */ 277 framework.ConformanceIt("works for multiple CRDs of different groups", func(ctx context.Context) { 278 ginkgo.By("CRs in different groups (two CRDs) show up in OpenAPI documentation") 279 crdFoo, err := setupCRD(f, schemaFoo, "foo", "v1") 280 if err != nil { 281 framework.Failf("%v", err) 282 } 283 crdWaldo, err := setupCRD(f, schemaWaldo, "waldo", "v1beta1") 284 if err != nil { 285 framework.Failf("%v", err) 286 } 287 if crdFoo.Crd.Spec.Group == crdWaldo.Crd.Spec.Group { 288 framework.Failf("unexpected: CRDs should be of different group %v, %v", crdFoo.Crd.Spec.Group, crdWaldo.Crd.Spec.Group) 289 } 290 if err := waitForDefinition(f.ClientSet, definitionName(crdWaldo, "v1beta1"), schemaWaldo); err != nil { 291 framework.Failf("%v", err) 292 } 293 if err := waitForDefinition(f.ClientSet, definitionName(crdFoo, "v1"), schemaFoo); err != nil { 294 framework.Failf("%v", err) 295 } 296 if err := cleanupCRD(ctx, f, crdFoo); err != nil { 297 framework.Failf("%v", err) 298 } 299 if err := cleanupCRD(ctx, f, crdWaldo); err != nil { 300 framework.Failf("%v", err) 301 } 302 }) 303 304 /* 305 Release: v1.16 306 Testname: Custom Resource OpenAPI Publish, varying versions 307 Description: Register a custom resource definition with multiple versions; OpenAPI definitions MUST be published 308 for custom resource definitions. 309 */ 310 framework.ConformanceIt("works for multiple CRDs of same group but different versions", func(ctx context.Context) { 311 ginkgo.By("CRs in the same group but different versions (one multiversion CRD) show up in OpenAPI documentation") 312 crdMultiVer, err := setupCRD(f, schemaFoo, "multi-ver", "v2", "v3") 313 if err != nil { 314 framework.Failf("%v", err) 315 } 316 if err := waitForDefinition(f.ClientSet, definitionName(crdMultiVer, "v3"), schemaFoo); err != nil { 317 framework.Failf("%v", err) 318 } 319 if err := waitForDefinition(f.ClientSet, definitionName(crdMultiVer, "v2"), schemaFoo); err != nil { 320 framework.Failf("%v", err) 321 } 322 if err := cleanupCRD(ctx, f, crdMultiVer); err != nil { 323 framework.Failf("%v", err) 324 } 325 326 ginkgo.By("CRs in the same group but different versions (two CRDs) show up in OpenAPI documentation") 327 crdFoo, err := setupCRD(f, schemaFoo, "common-group", "v4") 328 if err != nil { 329 framework.Failf("%v", err) 330 } 331 crdWaldo, err := setupCRD(f, schemaWaldo, "common-group", "v5") 332 if err != nil { 333 framework.Failf("%v", err) 334 } 335 if crdFoo.Crd.Spec.Group != crdWaldo.Crd.Spec.Group { 336 framework.Failf("unexpected: CRDs should be of the same group %v, %v", crdFoo.Crd.Spec.Group, crdWaldo.Crd.Spec.Group) 337 } 338 if err := waitForDefinition(f.ClientSet, definitionName(crdWaldo, "v5"), schemaWaldo); err != nil { 339 framework.Failf("%v", err) 340 } 341 if err := waitForDefinition(f.ClientSet, definitionName(crdFoo, "v4"), schemaFoo); err != nil { 342 framework.Failf("%v", err) 343 } 344 if err := cleanupCRD(ctx, f, crdFoo); err != nil { 345 framework.Failf("%v", err) 346 } 347 if err := cleanupCRD(ctx, f, crdWaldo); err != nil { 348 framework.Failf("%v", err) 349 } 350 }) 351 352 /* 353 Release: v1.16 354 Testname: Custom Resource OpenAPI Publish, varying kinds 355 Description: Register multiple custom resource definitions in the same group and version but spanning different kinds; 356 OpenAPI definitions MUST be published for custom resource definitions. 357 */ 358 framework.ConformanceIt("works for multiple CRDs of same group and version but different kinds", func(ctx context.Context) { 359 ginkgo.By("CRs in the same group and version but different kinds (two CRDs) show up in OpenAPI documentation") 360 crdFoo, err := setupCRD(f, schemaFoo, "common-group", "v6") 361 if err != nil { 362 framework.Failf("%v", err) 363 } 364 crdWaldo, err := setupCRD(f, schemaWaldo, "common-group", "v6") 365 if err != nil { 366 framework.Failf("%v", err) 367 } 368 if crdFoo.Crd.Spec.Group != crdWaldo.Crd.Spec.Group { 369 framework.Failf("unexpected: CRDs should be of the same group %v, %v", crdFoo.Crd.Spec.Group, crdWaldo.Crd.Spec.Group) 370 } 371 if err := waitForDefinition(f.ClientSet, definitionName(crdWaldo, "v6"), schemaWaldo); err != nil { 372 framework.Failf("%v", err) 373 } 374 if err := waitForDefinition(f.ClientSet, definitionName(crdFoo, "v6"), schemaFoo); err != nil { 375 framework.Failf("%v", err) 376 } 377 if err := cleanupCRD(ctx, f, crdFoo); err != nil { 378 framework.Failf("%v", err) 379 } 380 if err := cleanupCRD(ctx, f, crdWaldo); err != nil { 381 framework.Failf("%v", err) 382 } 383 }) 384 385 /* 386 Release: v1.16 387 Testname: Custom Resource OpenAPI Publish, version rename 388 Description: Register a custom resource definition with multiple versions; OpenAPI definitions MUST be published 389 for custom resource definitions. Rename one of the versions of the custom resource definition via a patch; 390 OpenAPI definitions MUST update to reflect the rename. 391 */ 392 framework.ConformanceIt("updates the published spec when one version gets renamed", func(ctx context.Context) { 393 ginkgo.By("set up a multi version CRD") 394 crdMultiVer, err := setupCRD(f, schemaFoo, "multi-ver", "v2", "v3") 395 if err != nil { 396 framework.Failf("%v", err) 397 } 398 if err := waitForDefinition(f.ClientSet, definitionName(crdMultiVer, "v3"), schemaFoo); err != nil { 399 framework.Failf("%v", err) 400 } 401 if err := waitForDefinition(f.ClientSet, definitionName(crdMultiVer, "v2"), schemaFoo); err != nil { 402 framework.Failf("%v", err) 403 } 404 405 ginkgo.By("rename a version") 406 patch := []byte(`[ 407 {"op":"test","path":"/spec/versions/1/name","value":"v3"}, 408 {"op": "replace", "path": "/spec/versions/1/name", "value": "v4"} 409 ]`) 410 crdMultiVer.Crd, err = crdMultiVer.APIExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Patch(ctx, crdMultiVer.Crd.Name, types.JSONPatchType, patch, metav1.PatchOptions{}) 411 if err != nil { 412 framework.Failf("%v", err) 413 } 414 415 ginkgo.By("check the new version name is served") 416 if err := waitForDefinition(f.ClientSet, definitionName(crdMultiVer, "v4"), schemaFoo); err != nil { 417 framework.Failf("%v", err) 418 } 419 ginkgo.By("check the old version name is removed") 420 if err := waitForDefinitionCleanup(f.ClientSet, definitionName(crdMultiVer, "v3")); err != nil { 421 framework.Failf("%v", err) 422 } 423 ginkgo.By("check the other version is not changed") 424 if err := waitForDefinition(f.ClientSet, definitionName(crdMultiVer, "v2"), schemaFoo); err != nil { 425 framework.Failf("%v", err) 426 } 427 428 // TestCrd.Versions is different from TestCrd.Crd.Versions, we have to manually 429 // update the name there. Used by cleanupCRD 430 crdMultiVer.Crd.Spec.Versions[1].Name = "v4" 431 if err := cleanupCRD(ctx, f, crdMultiVer); err != nil { 432 framework.Failf("%v", err) 433 } 434 }) 435 436 /* 437 Release: v1.16 438 Testname: Custom Resource OpenAPI Publish, stop serving version 439 Description: Register a custom resource definition with multiple versions. OpenAPI definitions MUST be published 440 for custom resource definitions. Update the custom resource definition to not serve one of the versions. OpenAPI 441 definitions MUST be updated to not contain the version that is no longer served. 442 */ 443 framework.ConformanceIt("removes definition from spec when one version gets changed to not be served", func(ctx context.Context) { 444 ginkgo.By("set up a multi version CRD") 445 crd, err := setupCRD(f, schemaFoo, "multi-to-single-ver", "v5", "v6alpha1") 446 if err != nil { 447 framework.Failf("%v", err) 448 } 449 // just double check. setupCRD() checked this for us already 450 if err := waitForDefinition(f.ClientSet, definitionName(crd, "v6alpha1"), schemaFoo); err != nil { 451 framework.Failf("%v", err) 452 } 453 if err := waitForDefinition(f.ClientSet, definitionName(crd, "v5"), schemaFoo); err != nil { 454 framework.Failf("%v", err) 455 } 456 457 ginkgo.By("mark a version not serverd") 458 crd.Crd, err = crd.APIExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, crd.Crd.Name, metav1.GetOptions{}) 459 if err != nil { 460 framework.Failf("%v", err) 461 } 462 crd.Crd.Spec.Versions[1].Served = false 463 crd.Crd, err = crd.APIExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Update(ctx, crd.Crd, metav1.UpdateOptions{}) 464 if err != nil { 465 framework.Failf("%v", err) 466 } 467 468 ginkgo.By("check the unserved version gets removed") 469 if err := waitForDefinitionCleanup(f.ClientSet, definitionName(crd, "v6alpha1")); err != nil { 470 framework.Failf("%v", err) 471 } 472 ginkgo.By("check the other version is not changed") 473 if err := waitForDefinition(f.ClientSet, definitionName(crd, "v5"), schemaFoo); err != nil { 474 framework.Failf("%v", err) 475 } 476 477 if err := cleanupCRD(ctx, f, crd); err != nil { 478 framework.Failf("%v", err) 479 } 480 }) 481 482 // Marked as flaky until https://github.com/kubernetes/kubernetes/issues/65517 is solved. 483 f.It(f.WithFlaky(), "kubectl explain works for CR with the same resource name as built-in object.", func(ctx context.Context) { 484 customServiceShortName := fmt.Sprintf("ksvc-%d", time.Now().Unix()) // make short name unique 485 opt := func(crd *apiextensionsv1.CustomResourceDefinition) { 486 crd.ObjectMeta = metav1.ObjectMeta{Name: "services." + crd.Spec.Group} 487 crd.Spec.Names = apiextensionsv1.CustomResourceDefinitionNames{ 488 Plural: "services", 489 Singular: "service", 490 ListKind: "ServiceList", 491 Kind: "Service", 492 ShortNames: []string{customServiceShortName}, 493 } 494 } 495 crdSvc, err := setupCRDAndVerifySchemaWithOptions(f, schemaCustomService, schemaCustomService, "service", []string{"v1"}, opt) 496 if err != nil { 497 framework.Failf("%v", err) 498 } 499 500 if err := verifyKubectlExplain(f.Namespace.Name, customServiceShortName+".spec", `(?s)DESCRIPTION:.*Specification of CustomService.*FIELDS:.*dummy.*<string>.*Dummy property`); err != nil { 501 _ = cleanupCRD(ctx, f, crdSvc) // need to remove the crd since its name is unchanged 502 framework.Failf("%v", err) 503 } 504 505 if err := cleanupCRD(ctx, f, crdSvc); err != nil { 506 framework.Failf("%v", err) 507 } 508 }) 509 }) 510 511 func setupCRD(f *framework.Framework, schema []byte, groupSuffix string, versions ...string) (*crd.TestCrd, error) { 512 expect := schema 513 if schema == nil { 514 // to be backwards compatible, we expect CRD controller to treat 515 // CRD with nil schema specially and publish an empty schema 516 expect = []byte(`type: object`) 517 } 518 return setupCRDAndVerifySchema(f, schema, expect, groupSuffix, versions...) 519 } 520 521 func setupCRDAndVerifySchema(f *framework.Framework, schema, expect []byte, groupSuffix string, versions ...string) (*crd.TestCrd, error) { 522 return setupCRDAndVerifySchemaWithOptions(f, schema, expect, groupSuffix, versions) 523 } 524 525 func setupCRDAndVerifySchemaWithOptions(f *framework.Framework, schema, expect []byte, groupSuffix string, versions []string, options ...crd.Option) (*crd.TestCrd, error) { 526 group := fmt.Sprintf("%s-test-%s.example.com", f.BaseName, groupSuffix) 527 if len(versions) == 0 { 528 return nil, fmt.Errorf("require at least one version for CRD") 529 } 530 531 props := &apiextensionsv1.JSONSchemaProps{} 532 if schema != nil { 533 if err := yaml.Unmarshal(schema, props); err != nil { 534 return nil, err 535 } 536 } 537 538 options = append(options, func(crd *apiextensionsv1.CustomResourceDefinition) { 539 var apiVersions []apiextensionsv1.CustomResourceDefinitionVersion 540 for i, version := range versions { 541 version := apiextensionsv1.CustomResourceDefinitionVersion{ 542 Name: version, 543 Served: true, 544 Storage: i == 0, 545 } 546 // set up validation when input schema isn't nil 547 if schema != nil { 548 version.Schema = &apiextensionsv1.CustomResourceValidation{ 549 OpenAPIV3Schema: props, 550 } 551 } else { 552 version.Schema = &apiextensionsv1.CustomResourceValidation{ 553 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ 554 XPreserveUnknownFields: pointer.BoolPtr(true), 555 Type: "object", 556 }, 557 } 558 } 559 apiVersions = append(apiVersions, version) 560 } 561 crd.Spec.Versions = apiVersions 562 }) 563 crd, err := crd.CreateMultiVersionTestCRD(f, group, options...) 564 if err != nil { 565 return nil, fmt.Errorf("failed to create CRD: %w", err) 566 } 567 568 for _, v := range crd.Crd.Spec.Versions { 569 if err := waitForDefinition(f.ClientSet, definitionName(crd, v.Name), expect); err != nil { 570 return nil, fmt.Errorf("%v", err) 571 } 572 } 573 return crd, nil 574 } 575 576 func cleanupCRD(ctx context.Context, f *framework.Framework, crd *crd.TestCrd) error { 577 _ = crd.CleanUp(ctx) 578 for _, v := range crd.Crd.Spec.Versions { 579 name := definitionName(crd, v.Name) 580 if err := waitForDefinitionCleanup(f.ClientSet, name); err != nil { 581 return fmt.Errorf("%v", err) 582 } 583 } 584 return nil 585 } 586 587 const waitSuccessThreshold = 10 588 589 // mustSucceedMultipleTimes calls f multiple times on success and only returns true if all calls are successful. 590 // This is necessary to avoid flaking tests where one call might hit a good apiserver while in HA other apiservers 591 // might be lagging behind. Calling f multiple times reduces the chance exponentially. 592 func mustSucceedMultipleTimes(n int, f func() (bool, error)) func() (bool, error) { 593 return func() (bool, error) { 594 for i := 0; i < n; i++ { 595 ok, err := f() 596 if err != nil || !ok { 597 return ok, err 598 } 599 } 600 return true, nil 601 } 602 } 603 604 // waitForDefinition waits for given definition showing up in swagger with given schema. 605 // If schema is nil, only the existence of the given name is checked. 606 func waitForDefinition(c k8sclientset.Interface, name string, schema []byte) error { 607 expect := spec.Schema{} 608 if err := convertJSONSchemaProps(schema, &expect); err != nil { 609 return err 610 } 611 612 err := waitForOpenAPISchema(c, func(spec *spec.Swagger) (bool, string) { 613 d, ok := spec.SwaggerProps.Definitions[name] 614 if !ok { 615 return false, fmt.Sprintf("spec.SwaggerProps.Definitions[\"%s\"] not found", name) 616 } 617 if schema != nil { 618 // drop properties and extension that we added 619 dropDefaults(&d) 620 if !apiequality.Semantic.DeepEqual(expect, d) { 621 return false, fmt.Sprintf("spec.SwaggerProps.Definitions[\"%s\"] not match; expect: %v, actual: %v", name, expect, d) 622 } 623 } 624 return true, "" 625 }) 626 if err != nil { 627 return fmt.Errorf("failed to wait for definition %q to be served with the right OpenAPI schema: %w", name, err) 628 } 629 return nil 630 } 631 632 // waitForDefinitionCleanup waits for given definition to be removed from swagger 633 func waitForDefinitionCleanup(c k8sclientset.Interface, name string) error { 634 err := waitForOpenAPISchema(c, func(spec *spec.Swagger) (bool, string) { 635 if _, ok := spec.SwaggerProps.Definitions[name]; ok { 636 return false, fmt.Sprintf("spec.SwaggerProps.Definitions[\"%s\"] still exists", name) 637 } 638 return true, "" 639 }) 640 if err != nil { 641 return fmt.Errorf("failed to wait for definition %q not to be served anymore: %w", name, err) 642 } 643 return nil 644 } 645 646 func waitForOpenAPISchema(c k8sclientset.Interface, pred func(*spec.Swagger) (bool, string)) error { 647 client := c.Discovery().RESTClient().(*rest.RESTClient).Client 648 url := c.Discovery().RESTClient().Get().AbsPath("openapi", "v2").URL() 649 lastMsg := "" 650 etag := "" 651 var etagSpec *spec.Swagger 652 if err := wait.Poll(500*time.Millisecond, 60*time.Second, mustSucceedMultipleTimes(waitSuccessThreshold, func() (bool, error) { 653 // download spec with etag support 654 spec := &spec.Swagger{} 655 req, err := http.NewRequest("GET", url.String(), nil) 656 if err != nil { 657 return false, err 658 } 659 req.Close = true // enforce a new connection to hit different HA API servers 660 if len(etag) > 0 { 661 req.Header.Set("If-None-Match", fmt.Sprintf(`"%s"`, etag)) 662 } 663 resp, err := client.Do(req) 664 if err != nil { 665 return false, err 666 } 667 defer resp.Body.Close() 668 if resp.StatusCode == http.StatusNotModified { 669 spec = etagSpec 670 } else if resp.StatusCode != http.StatusOK { 671 return false, fmt.Errorf("unexpected response: %d", resp.StatusCode) 672 } else if bs, err := io.ReadAll(resp.Body); err != nil { 673 return false, err 674 } else if err := json.Unmarshal(bs, spec); err != nil { 675 return false, err 676 } else { 677 etag = strings.Trim(resp.Header.Get("ETag"), `"`) 678 etagSpec = spec 679 } 680 681 var ok bool 682 ok, lastMsg = pred(spec) 683 return ok, nil 684 })); err != nil { 685 return fmt.Errorf("failed to wait for OpenAPI spec validating condition: %v; lastMsg: %s", err, lastMsg) 686 } 687 return nil 688 } 689 690 // convertJSONSchemaProps converts JSONSchemaProps in YAML to spec.Schema 691 func convertJSONSchemaProps(in []byte, out *spec.Schema) error { 692 external := apiextensionsv1.JSONSchemaProps{} 693 if err := yaml.UnmarshalStrict(in, &external); err != nil { 694 return err 695 } 696 internal := apiextensions.JSONSchemaProps{} 697 if err := apiextensionsv1.Convert_v1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(&external, &internal, nil); err != nil { 698 return err 699 } 700 kubeOut := spec.Schema{} 701 if err := validation.ConvertJSONSchemaPropsWithPostProcess(&internal, &kubeOut, validation.StripUnsupportedFormatsPostProcess); err != nil { 702 return err 703 } 704 bs, err := json.Marshal(kubeOut) 705 if err != nil { 706 return err 707 } 708 return json.Unmarshal(bs, out) 709 } 710 711 // dropDefaults drops properties and extension that we added to a schema 712 func dropDefaults(s *spec.Schema) { 713 delete(s.Properties, "metadata") 714 delete(s.Properties, "apiVersion") 715 delete(s.Properties, "kind") 716 delete(s.Extensions, "x-kubernetes-group-version-kind") 717 delete(s.Extensions, "x-kubernetes-selectable-fields") 718 } 719 720 func verifyKubectlExplain(ns, name, pattern string) error { 721 result, err := e2ekubectl.RunKubectl(ns, "explain", name) 722 if err != nil { 723 return fmt.Errorf("failed to explain %s: %w", name, err) 724 } 725 r := regexp.MustCompile(pattern) 726 if !r.Match([]byte(result)) { 727 return fmt.Errorf("kubectl explain %s result {%s} doesn't match pattern {%s}", name, result, pattern) 728 } 729 return nil 730 } 731 732 // definitionName returns the openapi definition name for given CRD in given version 733 func definitionName(crd *crd.TestCrd, version string) string { 734 return openapiutil.ToRESTFriendlyName(fmt.Sprintf("%s/%s/%s", crd.Crd.Spec.Group, version, crd.Crd.Spec.Names.Kind)) 735 } 736 737 var schemaFoo = []byte(`description: Foo CRD for Testing 738 type: object 739 properties: 740 spec: 741 type: object 742 description: Specification of Foo 743 properties: 744 bars: 745 description: List of Bars and their specs. 746 type: array 747 items: 748 type: object 749 required: 750 - name 751 properties: 752 name: 753 description: Name of Bar. 754 type: string 755 age: 756 description: Age of Bar. 757 type: string 758 feeling: 759 description: Whether Bar is feeling great. 760 type: string 761 enum: 762 - Great 763 - Down 764 bazs: 765 description: List of Bazs. 766 items: 767 type: string 768 type: array 769 status: 770 description: Status of Foo 771 type: object 772 properties: 773 bars: 774 description: List of Bars and their statuses. 775 type: array 776 items: 777 type: object 778 properties: 779 name: 780 description: Name of Bar. 781 type: string 782 available: 783 description: Whether the Bar is installed. 784 type: boolean 785 quxType: 786 description: Indicates to external qux type. 787 pattern: in-tree|out-of-tree 788 type: string`) 789 790 var schemaCustomService = []byte(`description: CustomService CRD for Testing 791 type: object 792 properties: 793 spec: 794 description: Specification of CustomService 795 type: object 796 properties: 797 dummy: 798 description: Dummy property. 799 type: string 800 `) 801 802 var schemaWaldo = []byte(`description: Waldo CRD for Testing 803 type: object 804 properties: 805 spec: 806 description: Specification of Waldo 807 type: object 808 properties: 809 dummy: 810 description: Dummy property. 811 type: object 812 status: 813 description: Status of Waldo 814 type: object 815 properties: 816 bars: 817 description: List of Bars and their statuses. 818 type: array 819 items: 820 type: object`) 821 822 var schemaPreserveRoot = []byte(`description: preserve-unknown-properties at root for Testing 823 x-kubernetes-preserve-unknown-fields: true 824 type: object 825 properties: 826 spec: 827 description: Specification of Waldo 828 type: object 829 properties: 830 dummy: 831 description: Dummy property. 832 type: object 833 status: 834 description: Status of Waldo 835 type: object 836 properties: 837 bars: 838 description: List of Bars and their statuses. 839 type: array 840 items: 841 type: object`) 842 843 var schemaPreserveNested = []byte(`description: preserve-unknown-properties in nested field for Testing 844 type: object 845 properties: 846 spec: 847 description: Specification of Waldo 848 type: object 849 x-kubernetes-preserve-unknown-fields: true 850 properties: 851 dummy: 852 description: Dummy property. 853 type: object 854 status: 855 description: Status of Waldo 856 type: object 857 properties: 858 bars: 859 description: List of Bars and their statuses. 860 type: array 861 items: 862 type: object`)