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  }