knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/resourcesemantics/conversion/conversion_test.go (about) 1 /* 2 Copyright 2020 The Knative 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 conversion 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "strings" 24 "testing" 25 26 // injection 27 _ "knative.dev/pkg/client/injection/apiextensions/informers/apiextensions/v1/customresourcedefinition/fake" 28 _ "knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret/fake" 29 30 "github.com/google/go-cmp/cmp" 31 "github.com/google/go-cmp/cmp/cmpopts" 32 apixv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 34 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 35 "k8s.io/apimachinery/pkg/runtime" 36 "k8s.io/apimachinery/pkg/runtime/schema" 37 "knative.dev/pkg/webhook" 38 "knative.dev/pkg/webhook/resourcesemantics/conversion/internal" 39 40 . "knative.dev/pkg/reconciler/testing" 41 ) 42 43 var ( 44 webhookPath = "/convert" 45 testGK = schema.GroupKind{ 46 Group: internal.Group, 47 Kind: internal.Kind, 48 } 49 50 zygotes = map[string]ConvertibleObject{ 51 "v1": &internal.V1Resource{}, 52 "v2": &internal.V2Resource{}, 53 "v3": &internal.V3Resource{}, 54 "error": &internal.ErrorResource{}, 55 } 56 57 kinds = map[schema.GroupKind]GroupKindConversion{ 58 testGK: { 59 DefinitionName: "resource.webhook.pkg.knative.dev", 60 HubVersion: "v1", 61 Zygotes: zygotes, 62 }, 63 } 64 65 rawOpt = cmp.Transformer("raw", func(res []runtime.RawExtension) []string { 66 result := make([]string, 0, len(res)) 67 for _, re := range res { 68 result = append(result, string(re.Raw)) 69 } 70 return result 71 }) 72 73 cmpOpts = []cmp.Option{ 74 rawOpt, 75 } 76 ) 77 78 func testAPIVersion(version string) string { 79 return testGK.WithVersion(version).GroupVersion().String() 80 } 81 82 func TestWebhookPath(t *testing.T) { 83 ctx, _ := SetupFakeContext(t) 84 ctx = webhook.WithOptions(ctx, webhook.Options{ 85 SecretName: "webhook-secret", 86 }) 87 88 controller := NewConversionController(ctx, "/some-path", nil, nil) 89 conversion := controller.Reconciler.(webhook.ConversionController) 90 91 if got, want := conversion.Path(), "/some-path"; got != want { 92 t.Errorf("expected controller to return provided path got: %q, want: %q", got, want) 93 } 94 } 95 96 func TestConversionToHub(t *testing.T) { 97 ctx, conversion := newConversion(t) 98 99 req := &apixv1.ConversionRequest{ 100 UID: "some-uid", 101 DesiredAPIVersion: testAPIVersion("v1"), 102 Objects: []runtime.RawExtension{ 103 toRaw(t, internal.NewV2("bing")), 104 toRaw(t, internal.NewV3("bang")), 105 }, 106 } 107 108 want := &apixv1.ConversionResponse{ 109 UID: "some-uid", 110 Result: metav1.Status{Status: metav1.StatusSuccess}, 111 ConvertedObjects: []runtime.RawExtension{ 112 toRaw(t, internal.NewV1("bing")), 113 toRaw(t, internal.NewV1("bang")), 114 }, 115 } 116 117 got := conversion.Convert(ctx, req) 118 if diff := cmp.Diff(want, got, cmpOpts...); diff != "" { 119 t.Error("unexpected response:", diff) 120 } 121 } 122 123 func TestConversionFromHub(t *testing.T) { 124 tests := []struct { 125 version string 126 in runtime.Object 127 out runtime.Object 128 }{{ 129 version: "v2", 130 in: internal.NewV1("bing"), 131 out: internal.NewV2("bing"), 132 }, { 133 version: "v3", 134 in: internal.NewV1("bing"), 135 out: internal.NewV3("bing"), 136 }} 137 138 for _, test := range tests { 139 t.Run(test.version, func(t *testing.T) { 140 ctx, conversion := newConversion(t) 141 req := &apixv1.ConversionRequest{ 142 UID: "some-uid", 143 DesiredAPIVersion: testAPIVersion(test.version), 144 Objects: []runtime.RawExtension{ 145 toRaw(t, test.in), 146 }, 147 } 148 149 want := &apixv1.ConversionResponse{ 150 UID: "some-uid", 151 Result: metav1.Status{Status: metav1.StatusSuccess}, 152 ConvertedObjects: []runtime.RawExtension{ 153 toRaw(t, test.out), 154 }, 155 } 156 157 got := conversion.Convert(ctx, req) 158 if diff := cmp.Diff(want, got, cmpOpts...); diff != "" { 159 t.Error("unexpected response:", diff) 160 } 161 }) 162 } 163 } 164 165 func TestConversionThroughHub(t *testing.T) { 166 tests := []struct { 167 name string 168 version string 169 in runtime.Object 170 out runtime.Object 171 }{{ 172 name: "v3 to v2", 173 version: "v2", 174 in: internal.NewV3("bing"), 175 out: internal.NewV2("bing"), 176 }, { 177 name: "v2 to v3", 178 version: "v3", 179 in: internal.NewV2("bang"), 180 out: internal.NewV3("bang"), 181 }} 182 183 for _, test := range tests { 184 t.Run(test.version, func(t *testing.T) { 185 ctx, conversion := newConversion(t) 186 187 req := &apixv1.ConversionRequest{ 188 UID: "some-uid", 189 DesiredAPIVersion: testAPIVersion(test.version), 190 Objects: []runtime.RawExtension{ 191 toRaw(t, test.in), 192 }, 193 } 194 195 want := &apixv1.ConversionResponse{ 196 UID: "some-uid", 197 Result: metav1.Status{Status: metav1.StatusSuccess}, 198 ConvertedObjects: []runtime.RawExtension{ 199 toRaw(t, test.out), 200 }, 201 } 202 203 got := conversion.Convert(ctx, req) 204 if diff := cmp.Diff(want, got, cmpOpts...); diff != "" { 205 t.Error("unexpected response:", diff) 206 } 207 }) 208 } 209 } 210 211 func TestConversionErrorBadGVK(t *testing.T) { 212 tests := []struct { 213 name string 214 gvk schema.GroupVersionKind 215 }{{ 216 name: "empty group", 217 gvk: schema.GroupVersionKind{ 218 Version: "v1", 219 Kind: "Resource", 220 }, 221 }, { 222 name: "empty version", 223 gvk: schema.GroupVersionKind{ 224 Group: "webhook.pkg.knative.dev", 225 Kind: "Resource", 226 }, 227 }, { 228 name: "empty kind", 229 gvk: schema.GroupVersionKind{ 230 Group: "webhook.pkg.knative.dev", 231 Version: "v1", 232 }, 233 }} 234 235 for _, test := range tests { 236 t.Run(test.name, func(t *testing.T) { 237 obj := internal.NewV2("bing") 238 obj.SetGroupVersionKind(test.gvk) 239 240 ctx, conversion := newConversion(t) 241 242 req := &apixv1.ConversionRequest{ 243 UID: "some-uid", 244 DesiredAPIVersion: testAPIVersion("v1"), 245 Objects: []runtime.RawExtension{ 246 toRaw(t, obj), 247 }, 248 } 249 250 want := &apixv1.ConversionResponse{ 251 UID: "some-uid", 252 Result: metav1.Status{ 253 Status: metav1.StatusFailure, 254 }, 255 } 256 257 cmpOpts := []cmp.Option{ 258 cmpopts.IgnoreFields(metav1.Status{}, "Message"), 259 cmpopts.EquateEmpty(), 260 } 261 262 got := conversion.Convert(ctx, req) 263 if diff := cmp.Diff(want, got, cmpOpts...); diff != "" { 264 t.Error("unexpected response:", diff) 265 } 266 267 if !strings.HasPrefix(got.Result.Message, "invalid GroupVersionKind") { 268 t.Errorf("expected message to start with 'invalid GroupVersionKind' got %q", got.Result.Message) 269 } 270 }) 271 } 272 } 273 274 func TestConversionUnknownInputGVK(t *testing.T) { 275 unknownObj := &unstructured.Unstructured{} 276 unknownObj.SetGroupVersionKind(schema.GroupVersionKind{ 277 Group: "some.api.group.dev", 278 Version: "v1", 279 Kind: "Resource", 280 }) 281 282 ctx, conversion := newConversion(t) 283 284 req := &apixv1.ConversionRequest{ 285 UID: "some-uid", 286 DesiredAPIVersion: testAPIVersion("v3"), 287 Objects: []runtime.RawExtension{ 288 toRaw(t, unknownObj), 289 }, 290 } 291 292 want := &apixv1.ConversionResponse{ 293 UID: "some-uid", 294 Result: metav1.Status{ 295 Message: "no conversion support for type [kind=Resource group=some.api.group.dev]", 296 Status: metav1.StatusFailure, 297 }, 298 } 299 300 got := conversion.Convert(ctx, req) 301 if diff := cmp.Diff(want, got, cmpOpts...); diff != "" { 302 t.Error("unexpected response:", diff) 303 } 304 } 305 306 func TestConversionInvalidTypeMeta(t *testing.T) { 307 ctx, conversion := newConversionWithKinds(t, nil) 308 309 req := &apixv1.ConversionRequest{ 310 UID: "some-uid", 311 DesiredAPIVersion: "some-version", 312 Objects: []runtime.RawExtension{ 313 {Raw: []byte("}")}, 314 }, 315 } 316 317 want := &apixv1.ConversionResponse{ 318 UID: "some-uid", 319 Result: metav1.Status{ 320 Status: metav1.StatusFailure, 321 }, 322 } 323 324 cmpOpts := []cmp.Option{ 325 cmpopts.IgnoreFields(metav1.Status{}, "Message"), 326 cmpopts.EquateEmpty(), 327 } 328 329 got := conversion.Convert(ctx, req) 330 if diff := cmp.Diff(want, got, cmpOpts...); diff != "" { 331 t.Error("unexpected response:", diff) 332 } 333 334 if !strings.HasPrefix(got.Result.Message, "error parsing type meta") { 335 t.Errorf("expected message to start with 'error parsing type meta' got %q", got.Result.Message) 336 } 337 } 338 339 func TestConversionFailureToUnmarshalInput(t *testing.T) { 340 ctx, conversion := newConversion(t) 341 342 req := &apixv1.ConversionRequest{ 343 UID: "some-uid", 344 DesiredAPIVersion: testAPIVersion("v1"), 345 Objects: []runtime.RawExtension{ 346 toRaw(t, internal.NewErrorResource(internal.ErrorUnmarshal)), 347 }, 348 } 349 350 want := &apixv1.ConversionResponse{ 351 UID: "some-uid", 352 Result: metav1.Status{ 353 Status: metav1.StatusFailure, 354 }, 355 } 356 357 cmpOpts := []cmp.Option{ 358 cmpopts.IgnoreFields(metav1.Status{}, "Message"), 359 cmpopts.EquateEmpty(), 360 } 361 362 got := conversion.Convert(ctx, req) 363 if diff := cmp.Diff(want, got, cmpOpts...); diff != "" { 364 t.Error("unexpected response:", diff) 365 } 366 367 if !strings.HasPrefix(got.Result.Message, "unable to unmarshal input") { 368 t.Errorf("expected message to start with 'unable to unmarshal input' got %q", got.Result.Message) 369 } 370 } 371 372 func TestConversionFailureToMarshalOutput(t *testing.T) { 373 ctx, conversion := newConversion(t) 374 375 req := &apixv1.ConversionRequest{ 376 UID: "some-uid", 377 DesiredAPIVersion: testAPIVersion("error"), 378 Objects: []runtime.RawExtension{ 379 // This property should make the Marshal on the 380 // ErrorResource to fail 381 toRaw(t, internal.NewV1(internal.ErrorMarshal)), 382 }, 383 } 384 385 want := &apixv1.ConversionResponse{ 386 UID: "some-uid", 387 Result: metav1.Status{ 388 Status: metav1.StatusFailure, 389 }, 390 } 391 392 cmpOpts := []cmp.Option{ 393 cmpopts.IgnoreFields(metav1.Status{}, "Message"), 394 cmpopts.EquateEmpty(), 395 } 396 397 got := conversion.Convert(ctx, req) 398 if diff := cmp.Diff(want, got, cmpOpts...); diff != "" { 399 t.Error("unexpected response:", diff) 400 } 401 402 if !strings.HasPrefix(got.Result.Message, "unable to marshal output") { 403 t.Errorf("expected message to start with 'unable to marshal output' got %q", got.Result.Message) 404 } 405 } 406 407 func TestConversionFailureToConvert(t *testing.T) { 408 // v1 => error resource => v3 409 kinds := map[schema.GroupKind]GroupKindConversion{ 410 testGK: { 411 DefinitionName: "resource.webhook.pkg.knative.dev", 412 HubVersion: "error", 413 Zygotes: zygotes, 414 }, 415 } 416 417 tests := []struct { 418 name string 419 errorOn string 420 }{{ 421 name: "error converting from", 422 errorOn: internal.ErrorConvertFrom, 423 }, { 424 name: "error converting to", 425 errorOn: internal.ErrorConvertTo, 426 }} 427 428 for _, test := range tests { 429 t.Run(test.name, func(t *testing.T) { 430 ctx, conversion := newConversionWithKinds(t, kinds) 431 req := &apixv1.ConversionRequest{ 432 UID: "some-uid", 433 DesiredAPIVersion: testAPIVersion("v3"), 434 Objects: []runtime.RawExtension{ 435 // Insert failure here 436 toRaw(t, internal.NewV1(test.errorOn)), 437 }, 438 } 439 440 want := &apixv1.ConversionResponse{ 441 UID: "some-uid", 442 Result: metav1.Status{ 443 Status: metav1.StatusFailure, 444 }, 445 } 446 447 cmpOpts := []cmp.Option{ 448 cmpopts.IgnoreFields(metav1.Status{}, "Message"), 449 rawOpt, 450 } 451 452 got := conversion.Convert(ctx, req) 453 if diff := cmp.Diff(want, got, cmpOpts...); diff != "" { 454 t.Error("unexpected response:", diff) 455 } 456 457 if !strings.HasPrefix(got.Result.Message, "conversion failed") { 458 t.Errorf("expected message to start with 'conversion failed' got %q", got.Result.Message) 459 } 460 }) 461 } 462 } 463 464 func TestConversionFailureInvalidDesiredAPIVersion(t *testing.T) { 465 tests := []struct { 466 name string 467 version string 468 }{{ 469 name: "multiple path segments", 470 version: "bad-api-version/v1/v2", 471 }, { 472 name: "empty", 473 version: "", 474 }, { 475 name: "path segment", 476 version: "/", 477 }, { 478 name: "no version", 479 version: "some.api.group", 480 }} 481 482 for _, test := range tests { 483 t.Run(test.name, func(t *testing.T) { 484 ctx, conversion := newConversion(t) 485 486 req := &apixv1.ConversionRequest{ 487 UID: "some-uid", 488 DesiredAPIVersion: test.version, 489 Objects: []runtime.RawExtension{ 490 toRaw(t, internal.NewV1("bing")), 491 }, 492 } 493 494 want := &apixv1.ConversionResponse{ 495 UID: "some-uid", 496 Result: metav1.Status{ 497 Message: fmt.Sprintf("desired API version %q is not valid", test.version), 498 Status: metav1.StatusFailure, 499 }, 500 } 501 502 got := conversion.Convert(ctx, req) 503 if diff := cmp.Diff(want, got, cmpOpts...); diff != "" { 504 t.Error("unexpected response:", diff) 505 } 506 }) 507 } 508 } 509 510 func TestConversionMissingZygotes(t *testing.T) { 511 // Assume we're converting from 512 // v2 (input) => v1 (hub) => v3 (output) 513 tests := []struct { 514 name string 515 zygotes map[string]ConvertibleObject 516 }{{ 517 name: "missing input", 518 zygotes: map[string]ConvertibleObject{ 519 "v1": &internal.V1Resource{}, 520 "v3": &internal.V3Resource{}, 521 }, 522 }, { 523 name: "missing output", 524 zygotes: map[string]ConvertibleObject{ 525 "v1": &internal.V1Resource{}, 526 "v2": &internal.V2Resource{}, 527 }, 528 }, { 529 name: "missing hub", 530 zygotes: map[string]ConvertibleObject{ 531 "v2": &internal.V2Resource{}, 532 "v3": &internal.V3Resource{}, 533 }, 534 }} 535 536 for _, test := range tests { 537 t.Run(test.name, func(t *testing.T) { 538 kinds = map[schema.GroupKind]GroupKindConversion{ 539 testGK: { 540 DefinitionName: "resource.webhook.pkg.knative.dev", 541 HubVersion: "v1", 542 Zygotes: test.zygotes, 543 }, 544 } 545 546 ctx, conversion := newConversionWithKinds(t, kinds) 547 548 req := &apixv1.ConversionRequest{ 549 UID: "some-uid", 550 DesiredAPIVersion: testAPIVersion("v3"), 551 Objects: []runtime.RawExtension{ 552 toRaw(t, internal.NewV2("bing")), 553 }, 554 } 555 556 want := &apixv1.ConversionResponse{ 557 UID: "some-uid", 558 Result: metav1.Status{ 559 Status: metav1.StatusFailure, 560 }, 561 } 562 563 cmpOpts := []cmp.Option{ 564 cmpopts.IgnoreFields(metav1.Status{}, "Message"), 565 cmpopts.EquateEmpty(), 566 } 567 568 got := conversion.Convert(ctx, req) 569 if diff := cmp.Diff(want, got, cmpOpts...); diff != "" { 570 t.Error("unexpected response:", diff) 571 } 572 573 if !strings.HasPrefix(got.Result.Message, "conversion not supported") { 574 t.Errorf("expected message to start with 'conversion not supported' got %q", got.Result.Message) 575 } 576 }) 577 } 578 } 579 580 func TestContextDecoration(t *testing.T) { 581 ctx, _ := SetupFakeContext(t) 582 ctx = webhook.WithOptions(ctx, webhook.Options{ 583 SecretName: "webhook-secret", 584 }) 585 586 decoratorCalled := false 587 decorator := func(ctx context.Context) context.Context { 588 decoratorCalled = true 589 return ctx 590 } 591 592 controller := NewConversionController(ctx, webhookPath, kinds, decorator) 593 r := controller.Reconciler.(*reconciler) 594 r.Convert(ctx, &apixv1.ConversionRequest{}) 595 596 if !decoratorCalled { 597 t.Errorf("context decorator was not invoked") 598 } 599 } 600 601 func toRaw(t *testing.T, obj runtime.Object) runtime.RawExtension { 602 t.Helper() 603 604 raw, err := json.Marshal(obj) 605 if err != nil { 606 t.Fatal("unable to marshal resource:", err) 607 } 608 609 return runtime.RawExtension{Raw: raw} 610 } 611 612 func newConversion(t *testing.T) (context.Context, webhook.ConversionController) { 613 return newConversionWithKinds(t, kinds) 614 } 615 616 func newConversionWithKinds( 617 t *testing.T, 618 kinds map[schema.GroupKind]GroupKindConversion, 619 ) ( 620 context.Context, 621 webhook.ConversionController, 622 ) { 623 ctx, _ := SetupFakeContext(t) 624 ctx = webhook.WithOptions(ctx, webhook.Options{ 625 SecretName: "webhook-secret", 626 }) 627 628 controller := NewConversionController(ctx, webhookPath, kinds, nil) 629 return ctx, controller.Reconciler.(*reconciler) 630 }