mirror of https://github.com/knative/pkg.git
631 lines
15 KiB
Go
631 lines
15 KiB
Go
/*
|
|
Copyright 2020 The Knative Authors
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package conversion
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
// injection
|
|
_ "knative.dev/pkg/client/injection/apiextensions/informers/apiextensions/v1/customresourcedefinition/fake"
|
|
_ "knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret/fake"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
apixv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"knative.dev/pkg/webhook"
|
|
"knative.dev/pkg/webhook/resourcesemantics/conversion/internal"
|
|
|
|
. "knative.dev/pkg/reconciler/testing"
|
|
)
|
|
|
|
var (
|
|
webhookPath = "/convert"
|
|
testGK = schema.GroupKind{
|
|
Group: internal.Group,
|
|
Kind: internal.Kind,
|
|
}
|
|
|
|
zygotes = map[string]ConvertibleObject{
|
|
"v1": &internal.V1Resource{},
|
|
"v2": &internal.V2Resource{},
|
|
"v3": &internal.V3Resource{},
|
|
"error": &internal.ErrorResource{},
|
|
}
|
|
|
|
kinds = map[schema.GroupKind]GroupKindConversion{
|
|
testGK: {
|
|
DefinitionName: "resource.webhook.pkg.knative.dev",
|
|
HubVersion: "v1",
|
|
Zygotes: zygotes,
|
|
},
|
|
}
|
|
|
|
rawOpt = cmp.Transformer("raw", func(res []runtime.RawExtension) []string {
|
|
result := make([]string, 0, len(res))
|
|
for _, re := range res {
|
|
result = append(result, string(re.Raw))
|
|
}
|
|
return result
|
|
})
|
|
|
|
cmpOpts = []cmp.Option{
|
|
rawOpt,
|
|
}
|
|
)
|
|
|
|
func testAPIVersion(version string) string {
|
|
return testGK.WithVersion(version).GroupVersion().String()
|
|
}
|
|
|
|
func TestWebhookPath(t *testing.T) {
|
|
ctx, _ := SetupFakeContext(t)
|
|
ctx = webhook.WithOptions(ctx, webhook.Options{
|
|
SecretName: "webhook-secret",
|
|
})
|
|
|
|
controller := NewConversionController(ctx, "/some-path", nil, nil)
|
|
conversion := controller.Reconciler.(webhook.ConversionController)
|
|
|
|
if got, want := conversion.Path(), "/some-path"; got != want {
|
|
t.Errorf("expected controller to return provided path got: %q, want: %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestConversionToHub(t *testing.T) {
|
|
ctx, conversion := newConversion(t)
|
|
|
|
req := &apixv1.ConversionRequest{
|
|
UID: "some-uid",
|
|
DesiredAPIVersion: testAPIVersion("v1"),
|
|
Objects: []runtime.RawExtension{
|
|
toRaw(t, internal.NewV2("bing")),
|
|
toRaw(t, internal.NewV3("bang")),
|
|
},
|
|
}
|
|
|
|
want := &apixv1.ConversionResponse{
|
|
UID: "some-uid",
|
|
Result: metav1.Status{Status: metav1.StatusSuccess},
|
|
ConvertedObjects: []runtime.RawExtension{
|
|
toRaw(t, internal.NewV1("bing")),
|
|
toRaw(t, internal.NewV1("bang")),
|
|
},
|
|
}
|
|
|
|
got := conversion.Convert(ctx, req)
|
|
if diff := cmp.Diff(want, got, cmpOpts...); diff != "" {
|
|
t.Error("unexpected response:", diff)
|
|
}
|
|
}
|
|
|
|
func TestConversionFromHub(t *testing.T) {
|
|
tests := []struct {
|
|
version string
|
|
in runtime.Object
|
|
out runtime.Object
|
|
}{{
|
|
version: "v2",
|
|
in: internal.NewV1("bing"),
|
|
out: internal.NewV2("bing"),
|
|
}, {
|
|
version: "v3",
|
|
in: internal.NewV1("bing"),
|
|
out: internal.NewV3("bing"),
|
|
}}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.version, func(t *testing.T) {
|
|
ctx, conversion := newConversion(t)
|
|
req := &apixv1.ConversionRequest{
|
|
UID: "some-uid",
|
|
DesiredAPIVersion: testAPIVersion(test.version),
|
|
Objects: []runtime.RawExtension{
|
|
toRaw(t, test.in),
|
|
},
|
|
}
|
|
|
|
want := &apixv1.ConversionResponse{
|
|
UID: "some-uid",
|
|
Result: metav1.Status{Status: metav1.StatusSuccess},
|
|
ConvertedObjects: []runtime.RawExtension{
|
|
toRaw(t, test.out),
|
|
},
|
|
}
|
|
|
|
got := conversion.Convert(ctx, req)
|
|
if diff := cmp.Diff(want, got, cmpOpts...); diff != "" {
|
|
t.Error("unexpected response:", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConversionThroughHub(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
version string
|
|
in runtime.Object
|
|
out runtime.Object
|
|
}{{
|
|
name: "v3 to v2",
|
|
version: "v2",
|
|
in: internal.NewV3("bing"),
|
|
out: internal.NewV2("bing"),
|
|
}, {
|
|
name: "v2 to v3",
|
|
version: "v3",
|
|
in: internal.NewV2("bang"),
|
|
out: internal.NewV3("bang"),
|
|
}}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.version, func(t *testing.T) {
|
|
ctx, conversion := newConversion(t)
|
|
|
|
req := &apixv1.ConversionRequest{
|
|
UID: "some-uid",
|
|
DesiredAPIVersion: testAPIVersion(test.version),
|
|
Objects: []runtime.RawExtension{
|
|
toRaw(t, test.in),
|
|
},
|
|
}
|
|
|
|
want := &apixv1.ConversionResponse{
|
|
UID: "some-uid",
|
|
Result: metav1.Status{Status: metav1.StatusSuccess},
|
|
ConvertedObjects: []runtime.RawExtension{
|
|
toRaw(t, test.out),
|
|
},
|
|
}
|
|
|
|
got := conversion.Convert(ctx, req)
|
|
if diff := cmp.Diff(want, got, cmpOpts...); diff != "" {
|
|
t.Error("unexpected response:", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConversionErrorBadGVK(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
gvk schema.GroupVersionKind
|
|
}{{
|
|
name: "empty group",
|
|
gvk: schema.GroupVersionKind{
|
|
Version: "v1",
|
|
Kind: "Resource",
|
|
},
|
|
}, {
|
|
name: "empty version",
|
|
gvk: schema.GroupVersionKind{
|
|
Group: "webhook.pkg.knative.dev",
|
|
Kind: "Resource",
|
|
},
|
|
}, {
|
|
name: "empty kind",
|
|
gvk: schema.GroupVersionKind{
|
|
Group: "webhook.pkg.knative.dev",
|
|
Version: "v1",
|
|
},
|
|
}}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
obj := internal.NewV2("bing")
|
|
obj.SetGroupVersionKind(test.gvk)
|
|
|
|
ctx, conversion := newConversion(t)
|
|
|
|
req := &apixv1.ConversionRequest{
|
|
UID: "some-uid",
|
|
DesiredAPIVersion: testAPIVersion("v1"),
|
|
Objects: []runtime.RawExtension{
|
|
toRaw(t, obj),
|
|
},
|
|
}
|
|
|
|
want := &apixv1.ConversionResponse{
|
|
UID: "some-uid",
|
|
Result: metav1.Status{
|
|
Status: metav1.StatusFailure,
|
|
},
|
|
}
|
|
|
|
cmpOpts := []cmp.Option{
|
|
cmpopts.IgnoreFields(metav1.Status{}, "Message"),
|
|
cmpopts.EquateEmpty(),
|
|
}
|
|
|
|
got := conversion.Convert(ctx, req)
|
|
if diff := cmp.Diff(want, got, cmpOpts...); diff != "" {
|
|
t.Error("unexpected response:", diff)
|
|
}
|
|
|
|
if !strings.HasPrefix(got.Result.Message, "invalid GroupVersionKind") {
|
|
t.Errorf("expected message to start with 'invalid GroupVersionKind' got %q", got.Result.Message)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConversionUnknownInputGVK(t *testing.T) {
|
|
unknownObj := &unstructured.Unstructured{}
|
|
unknownObj.SetGroupVersionKind(schema.GroupVersionKind{
|
|
Group: "some.api.group.dev",
|
|
Version: "v1",
|
|
Kind: "Resource",
|
|
})
|
|
|
|
ctx, conversion := newConversion(t)
|
|
|
|
req := &apixv1.ConversionRequest{
|
|
UID: "some-uid",
|
|
DesiredAPIVersion: testAPIVersion("v3"),
|
|
Objects: []runtime.RawExtension{
|
|
toRaw(t, unknownObj),
|
|
},
|
|
}
|
|
|
|
want := &apixv1.ConversionResponse{
|
|
UID: "some-uid",
|
|
Result: metav1.Status{
|
|
Message: "no conversion support for type [kind=Resource group=some.api.group.dev]",
|
|
Status: metav1.StatusFailure,
|
|
},
|
|
}
|
|
|
|
got := conversion.Convert(ctx, req)
|
|
if diff := cmp.Diff(want, got, cmpOpts...); diff != "" {
|
|
t.Error("unexpected response:", diff)
|
|
}
|
|
}
|
|
|
|
func TestConversionInvalidTypeMeta(t *testing.T) {
|
|
ctx, conversion := newConversionWithKinds(t, nil)
|
|
|
|
req := &apixv1.ConversionRequest{
|
|
UID: "some-uid",
|
|
DesiredAPIVersion: "some-version",
|
|
Objects: []runtime.RawExtension{
|
|
{Raw: []byte("}")},
|
|
},
|
|
}
|
|
|
|
want := &apixv1.ConversionResponse{
|
|
UID: "some-uid",
|
|
Result: metav1.Status{
|
|
Status: metav1.StatusFailure,
|
|
},
|
|
}
|
|
|
|
cmpOpts := []cmp.Option{
|
|
cmpopts.IgnoreFields(metav1.Status{}, "Message"),
|
|
cmpopts.EquateEmpty(),
|
|
}
|
|
|
|
got := conversion.Convert(ctx, req)
|
|
if diff := cmp.Diff(want, got, cmpOpts...); diff != "" {
|
|
t.Error("unexpected response:", diff)
|
|
}
|
|
|
|
if !strings.HasPrefix(got.Result.Message, "error parsing type meta") {
|
|
t.Errorf("expected message to start with 'error parsing type meta' got %q", got.Result.Message)
|
|
}
|
|
}
|
|
|
|
func TestConversionFailureToUnmarshalInput(t *testing.T) {
|
|
ctx, conversion := newConversion(t)
|
|
|
|
req := &apixv1.ConversionRequest{
|
|
UID: "some-uid",
|
|
DesiredAPIVersion: testAPIVersion("v1"),
|
|
Objects: []runtime.RawExtension{
|
|
toRaw(t, internal.NewErrorResource(internal.ErrorUnmarshal)),
|
|
},
|
|
}
|
|
|
|
want := &apixv1.ConversionResponse{
|
|
UID: "some-uid",
|
|
Result: metav1.Status{
|
|
Status: metav1.StatusFailure,
|
|
},
|
|
}
|
|
|
|
cmpOpts := []cmp.Option{
|
|
cmpopts.IgnoreFields(metav1.Status{}, "Message"),
|
|
cmpopts.EquateEmpty(),
|
|
}
|
|
|
|
got := conversion.Convert(ctx, req)
|
|
if diff := cmp.Diff(want, got, cmpOpts...); diff != "" {
|
|
t.Error("unexpected response:", diff)
|
|
}
|
|
|
|
if !strings.HasPrefix(got.Result.Message, "unable to unmarshal input") {
|
|
t.Errorf("expected message to start with 'unable to unmarshal input' got %q", got.Result.Message)
|
|
}
|
|
}
|
|
|
|
func TestConversionFailureToMarshalOutput(t *testing.T) {
|
|
ctx, conversion := newConversion(t)
|
|
|
|
req := &apixv1.ConversionRequest{
|
|
UID: "some-uid",
|
|
DesiredAPIVersion: testAPIVersion("error"),
|
|
Objects: []runtime.RawExtension{
|
|
// This property should make the Marshal on the
|
|
// ErrorResource to fail
|
|
toRaw(t, internal.NewV1(internal.ErrorMarshal)),
|
|
},
|
|
}
|
|
|
|
want := &apixv1.ConversionResponse{
|
|
UID: "some-uid",
|
|
Result: metav1.Status{
|
|
Status: metav1.StatusFailure,
|
|
},
|
|
}
|
|
|
|
cmpOpts := []cmp.Option{
|
|
cmpopts.IgnoreFields(metav1.Status{}, "Message"),
|
|
cmpopts.EquateEmpty(),
|
|
}
|
|
|
|
got := conversion.Convert(ctx, req)
|
|
if diff := cmp.Diff(want, got, cmpOpts...); diff != "" {
|
|
t.Error("unexpected response:", diff)
|
|
}
|
|
|
|
if !strings.HasPrefix(got.Result.Message, "unable to marshal output") {
|
|
t.Errorf("expected message to start with 'unable to marshal output' got %q", got.Result.Message)
|
|
}
|
|
}
|
|
|
|
func TestConversionFailureToConvert(t *testing.T) {
|
|
// v1 => error resource => v3
|
|
kinds := map[schema.GroupKind]GroupKindConversion{
|
|
testGK: {
|
|
DefinitionName: "resource.webhook.pkg.knative.dev",
|
|
HubVersion: "error",
|
|
Zygotes: zygotes,
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
errorOn string
|
|
}{{
|
|
name: "error converting from",
|
|
errorOn: internal.ErrorConvertFrom,
|
|
}, {
|
|
name: "error converting to",
|
|
errorOn: internal.ErrorConvertTo,
|
|
}}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
ctx, conversion := newConversionWithKinds(t, kinds)
|
|
req := &apixv1.ConversionRequest{
|
|
UID: "some-uid",
|
|
DesiredAPIVersion: testAPIVersion("v3"),
|
|
Objects: []runtime.RawExtension{
|
|
// Insert failure here
|
|
toRaw(t, internal.NewV1(test.errorOn)),
|
|
},
|
|
}
|
|
|
|
want := &apixv1.ConversionResponse{
|
|
UID: "some-uid",
|
|
Result: metav1.Status{
|
|
Status: metav1.StatusFailure,
|
|
},
|
|
}
|
|
|
|
cmpOpts := []cmp.Option{
|
|
cmpopts.IgnoreFields(metav1.Status{}, "Message"),
|
|
rawOpt,
|
|
}
|
|
|
|
got := conversion.Convert(ctx, req)
|
|
if diff := cmp.Diff(want, got, cmpOpts...); diff != "" {
|
|
t.Error("unexpected response:", diff)
|
|
}
|
|
|
|
if !strings.HasPrefix(got.Result.Message, "conversion failed") {
|
|
t.Errorf("expected message to start with 'conversion failed' got %q", got.Result.Message)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConversionFailureInvalidDesiredAPIVersion(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
version string
|
|
}{{
|
|
name: "multiple path segments",
|
|
version: "bad-api-version/v1/v2",
|
|
}, {
|
|
name: "empty",
|
|
version: "",
|
|
}, {
|
|
name: "path segment",
|
|
version: "/",
|
|
}, {
|
|
name: "no version",
|
|
version: "some.api.group",
|
|
}}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
ctx, conversion := newConversion(t)
|
|
|
|
req := &apixv1.ConversionRequest{
|
|
UID: "some-uid",
|
|
DesiredAPIVersion: test.version,
|
|
Objects: []runtime.RawExtension{
|
|
toRaw(t, internal.NewV1("bing")),
|
|
},
|
|
}
|
|
|
|
want := &apixv1.ConversionResponse{
|
|
UID: "some-uid",
|
|
Result: metav1.Status{
|
|
Message: fmt.Sprintf("desired API version %q is not valid", test.version),
|
|
Status: metav1.StatusFailure,
|
|
},
|
|
}
|
|
|
|
got := conversion.Convert(ctx, req)
|
|
if diff := cmp.Diff(want, got, cmpOpts...); diff != "" {
|
|
t.Error("unexpected response:", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConversionMissingZygotes(t *testing.T) {
|
|
// Assume we're converting from
|
|
// v2 (input) => v1 (hub) => v3 (output)
|
|
tests := []struct {
|
|
name string
|
|
zygotes map[string]ConvertibleObject
|
|
}{{
|
|
name: "missing input",
|
|
zygotes: map[string]ConvertibleObject{
|
|
"v1": &internal.V1Resource{},
|
|
"v3": &internal.V3Resource{},
|
|
},
|
|
}, {
|
|
name: "missing output",
|
|
zygotes: map[string]ConvertibleObject{
|
|
"v1": &internal.V1Resource{},
|
|
"v2": &internal.V2Resource{},
|
|
},
|
|
}, {
|
|
name: "missing hub",
|
|
zygotes: map[string]ConvertibleObject{
|
|
"v2": &internal.V2Resource{},
|
|
"v3": &internal.V3Resource{},
|
|
},
|
|
}}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
kinds = map[schema.GroupKind]GroupKindConversion{
|
|
testGK: {
|
|
DefinitionName: "resource.webhook.pkg.knative.dev",
|
|
HubVersion: "v1",
|
|
Zygotes: test.zygotes,
|
|
},
|
|
}
|
|
|
|
ctx, conversion := newConversionWithKinds(t, kinds)
|
|
|
|
req := &apixv1.ConversionRequest{
|
|
UID: "some-uid",
|
|
DesiredAPIVersion: testAPIVersion("v3"),
|
|
Objects: []runtime.RawExtension{
|
|
toRaw(t, internal.NewV2("bing")),
|
|
},
|
|
}
|
|
|
|
want := &apixv1.ConversionResponse{
|
|
UID: "some-uid",
|
|
Result: metav1.Status{
|
|
Status: metav1.StatusFailure,
|
|
},
|
|
}
|
|
|
|
cmpOpts := []cmp.Option{
|
|
cmpopts.IgnoreFields(metav1.Status{}, "Message"),
|
|
cmpopts.EquateEmpty(),
|
|
}
|
|
|
|
got := conversion.Convert(ctx, req)
|
|
if diff := cmp.Diff(want, got, cmpOpts...); diff != "" {
|
|
t.Error("unexpected response:", diff)
|
|
}
|
|
|
|
if !strings.HasPrefix(got.Result.Message, "conversion not supported") {
|
|
t.Errorf("expected message to start with 'conversion not supported' got %q", got.Result.Message)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestContextDecoration(t *testing.T) {
|
|
ctx, _ := SetupFakeContext(t)
|
|
ctx = webhook.WithOptions(ctx, webhook.Options{
|
|
SecretName: "webhook-secret",
|
|
})
|
|
|
|
decoratorCalled := false
|
|
decorator := func(ctx context.Context) context.Context {
|
|
decoratorCalled = true
|
|
return ctx
|
|
}
|
|
|
|
controller := NewConversionController(ctx, webhookPath, kinds, decorator)
|
|
r := controller.Reconciler.(*reconciler)
|
|
r.Convert(ctx, &apixv1.ConversionRequest{})
|
|
|
|
if !decoratorCalled {
|
|
t.Errorf("context decorator was not invoked")
|
|
}
|
|
}
|
|
|
|
func toRaw(t *testing.T, obj runtime.Object) runtime.RawExtension {
|
|
t.Helper()
|
|
|
|
raw, err := json.Marshal(obj)
|
|
if err != nil {
|
|
t.Fatal("unable to marshal resource:", err)
|
|
}
|
|
|
|
return runtime.RawExtension{Raw: raw}
|
|
}
|
|
|
|
func newConversion(t *testing.T) (context.Context, webhook.ConversionController) {
|
|
return newConversionWithKinds(t, kinds)
|
|
}
|
|
|
|
func newConversionWithKinds(
|
|
t *testing.T,
|
|
kinds map[schema.GroupKind]GroupKindConversion,
|
|
) (
|
|
context.Context,
|
|
webhook.ConversionController,
|
|
) {
|
|
ctx, _ := SetupFakeContext(t)
|
|
ctx = webhook.WithOptions(ctx, webhook.Options{
|
|
SecretName: "webhook-secret",
|
|
})
|
|
|
|
controller := NewConversionController(ctx, webhookPath, kinds, nil)
|
|
return ctx, controller.Reconciler.(*reconciler)
|
|
}
|