pkg/resolver/addressable_resolver_test.go

1332 lines
40 KiB
Go

/*
Copyright 2019 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 resolver_test
import (
"context"
"errors"
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
"knative.dev/pkg/apis"
duckv1 "knative.dev/pkg/apis/duck/v1"
duckv1alpha1 "knative.dev/pkg/apis/duck/v1alpha1"
duckv1beta1 "knative.dev/pkg/apis/duck/v1beta1"
"knative.dev/pkg/client/injection/ducks/duck/v1/addressable"
fakedynamicclient "knative.dev/pkg/injection/clients/dynamicclient/fake"
"knative.dev/pkg/resolver"
"knative.dev/pkg/tracker"
)
const (
addressableDNS = "http://addressable.sink.svc.cluster.local"
addressableDNS1 = "http://addressable.sink1.svc.cluster.local"
addressableDNS2 = "http://addressable.sink2.svc.cluster.local"
addressableDNS3 = "http://addressable.sink3.svc.cluster.local"
addressableDNSWithPathAndTrailingSlash = "http://addressable.sink.svc.cluster.local/bar/"
addressableDNSWithPathAndNoTrailingSlash = "http://addressable.sink.svc.cluster.local/bar"
addressableName = "testsink"
addressableName1 = "testsink1"
addressableName2 = "testsink2"
addressableName3 = "testsink3"
addressableKind = "Sink"
addressableAPIVersion = "duck.knative.dev/v1"
unaddressableName = "testunaddressable"
unaddressableKind = "KResource"
unaddressableAPIVersion = "duck.knative.dev/v1alpha1"
unaddressableResource = "kresources.duck.knative.dev"
testNS = "testnamespace"
CACert = `-----BEGIN CERTIFICATE-----
MIICNDCCAaECEAKtZn5ORf5eV288mBle3cAwDQYJKoZIhvcNAQECBQAwXzELMAkG
A1UEBhMCVVMxIDAeBgNVBAoTF1JTQSBEYXRhIFNlY3VyaXR5LCBJbmMuMS4wLAYD
VQQLEyVTZWN1cmUgU2VydmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk0
MTEwOTAwMDAwMFoXDTEwMDEwNzIzNTk1OVowXzELMAkGA1UEBhMCVVMxIDAeBgNV
BAoTF1JTQSBEYXRhIFNlY3VyaXR5LCBJbmMuMS4wLAYDVQQLEyVTZWN1cmUgU2Vy
dmVyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGbMA0GCSqGSIb3DQEBAQUAA4GJ
ADCBhQJ+AJLOesGugz5aqomDV6wlAXYMra6OLDfO6zV4ZFQD5YRAUcm/jwjiioII
0haGN1XpsSECrXZogZoFokvJSyVmIlZsiAeP94FZbYQHZXATcXY+m3dM41CJVphI
uR2nKRoTLkoRWZweFdVJVCxzOmmCsZc5nG1wZ0jl3S3WyB57AgMBAAEwDQYJKoZI
hvcNAQECBQADfgBl3X7hsuyw4jrg7HFGmhkRuNPHoLQDQCYCPgmc4RKz0Vr2N6W3
YQO2WxZpO8ZECAyIUwxrl0nHPjXcbLm7qt9cuzovk2C2qUtN8iD3zV9/ZHuO3ABc
1/p3yjkWWW8O6tO1g39NTUJWdrTJXwT4OPjr0l91X817/OWOgHz8UA==
-----END CERTIFICATE-----`
addressableAudience = "my-audience"
)
func init() {
// Add types to scheme
duckv1alpha1.AddToScheme(scheme.Scheme)
duckv1beta1.AddToScheme(scheme.Scheme)
scheme.Scheme.AddKnownTypeWithName(
schema.FromAPIVersionAndKind(unaddressableAPIVersion, unaddressableKind),
&unstructured.Unstructured{},
)
scheme.Scheme.AddKnownTypeWithName(
schema.FromAPIVersionAndKind(unaddressableAPIVersion, unaddressableKind+"List"),
&unstructured.UnstructuredList{},
)
scheme.Scheme.AddKnownTypeWithName(
schema.FromAPIVersionAndKind(addressableAPIVersion, addressableKind),
&unstructured.Unstructured{},
)
scheme.Scheme.AddKnownTypeWithName(
schema.FromAPIVersionAndKind(addressableAPIVersion, addressableKind+"List"),
&unstructured.UnstructuredList{},
)
}
func TestGetURIDestinationV1Beta1(t *testing.T) {
tests := map[string]struct {
objects []runtime.Object
dest duckv1beta1.Destination
wantURI string
wantErr string
}{
"nil everything": {
wantErr: "destination missing Ref and URI, expected at least one",
}, "Happy URI with path": {
dest: duckv1beta1.Destination{
URI: &apis.URL{
Scheme: "http",
Host: "example.com",
Path: "/foo",
},
},
wantURI: "http://example.com/foo",
}, "URI is not absolute URL": {
dest: duckv1beta1.Destination{
URI: &apis.URL{
Host: "example.com",
},
},
wantErr: fmt.Sprintf("URI is not absolute (both scheme and host should be non-empty): %q", "//example.com"),
}, "URI with no host": {
dest: duckv1beta1.Destination{
URI: &apis.URL{
Scheme: "http",
},
},
wantErr: fmt.Sprintf("URI is not absolute (both scheme and host should be non-empty): %q", "http:"),
},
"Ref and [apiVersion, kind, name] both exists": {
objects: []runtime.Object{
getAddressable(),
},
dest: duckv1beta1.Destination{
Ref: addressableRef(),
DeprecatedKind: addressableKind,
DeprecatedName: addressableName,
DeprecatedAPIVersion: addressableAPIVersion,
DeprecatedNamespace: testNS,
},
wantErr: "ref and [apiVersion, kind, name] can't be both present",
},
"happy ref": {
objects: []runtime.Object{
getAddressable(),
},
dest: duckv1beta1.Destination{Ref: addressableRef()},
wantURI: addressableDNS,
}, "ref with relative uri": {
objects: []runtime.Object{
getAddressable(),
},
dest: duckv1beta1.Destination{
Ref: addressableRef(),
URI: &apis.URL{
Path: "/foo",
},
},
wantURI: addressableDNS + "/foo",
}, "ref with relative URI without leading slash": {
objects: []runtime.Object{
getAddressable(),
},
dest: duckv1beta1.Destination{
Ref: addressableRef(),
URI: &apis.URL{
Path: "foo",
},
},
wantURI: addressableDNS + "/foo",
}, "ref ends with path and trailing slash and relative URI without leading slash ": {
objects: []runtime.Object{
addressableWithPathAndTrailingSlash(),
},
dest: duckv1beta1.Destination{
Ref: addressableRef(),
URI: &apis.URL{
Path: "foo",
},
},
wantURI: addressableDNSWithPathAndTrailingSlash + "foo",
}, "ref ends with path and trailing slash and relative URI with leading slash ": {
objects: []runtime.Object{
addressableWithPathAndTrailingSlash(),
},
dest: duckv1beta1.Destination{
Ref: addressableRef(),
URI: &apis.URL{
Path: "/foo",
},
},
wantURI: addressableDNS + "/foo",
}, "ref ends with path and no trailing slash and relative URI without leading slash ": {
objects: []runtime.Object{
addressableWithPathAndNoTrailingSlash(),
},
dest: duckv1beta1.Destination{
Ref: addressableRef(),
URI: &apis.URL{
Path: "foo",
},
},
wantURI: addressableDNS + "/foo",
}, "ref ends with path and no trailing slash and relative URI with leading slash ": {
objects: []runtime.Object{
addressableWithPathAndNoTrailingSlash(),
},
dest: duckv1beta1.Destination{
Ref: addressableRef(),
URI: &apis.URL{
Path: "/foo",
},
},
wantURI: addressableDNS + "/foo",
}, "ref with URI which is absolute URL": {
objects: []runtime.Object{
getAddressable(),
},
dest: duckv1beta1.Destination{
Ref: addressableRef(),
URI: &apis.URL{
Scheme: "http",
Host: "example.com",
Path: "/foo",
},
},
wantErr: "absolute URI is not allowed when Ref or [apiVersion, kind, name] exists",
},
"happy [apiVersion, kind, name]": {
objects: []runtime.Object{
getAddressable(),
},
dest: duckv1beta1.Destination{
DeprecatedKind: addressableKind,
DeprecatedName: addressableName,
DeprecatedAPIVersion: addressableAPIVersion,
DeprecatedNamespace: testNS,
},
wantURI: addressableDNS,
},
"[apiVersion, kind, name] with relative uri": {
objects: []runtime.Object{
getAddressable(),
},
dest: duckv1beta1.Destination{
DeprecatedKind: addressableKind,
DeprecatedName: addressableName,
DeprecatedAPIVersion: addressableAPIVersion,
DeprecatedNamespace: testNS,
URI: &apis.URL{
Path: "/foo",
},
},
wantURI: addressableDNS + "/foo",
}, "[apiVersion, kind, name] with relative URI without leading slash": {
objects: []runtime.Object{
getAddressable(),
},
dest: duckv1beta1.Destination{
DeprecatedKind: addressableKind,
DeprecatedName: addressableName,
DeprecatedAPIVersion: addressableAPIVersion,
DeprecatedNamespace: testNS,
URI: &apis.URL{
Path: "foo",
},
},
wantURI: addressableDNS + "/foo",
}, "[apiVersion, kind, name] ends with path and trailing slash and relative URI without leading slash ": {
objects: []runtime.Object{
addressableWithPathAndTrailingSlash(),
},
dest: duckv1beta1.Destination{
DeprecatedKind: addressableKind,
DeprecatedName: addressableName,
DeprecatedAPIVersion: addressableAPIVersion,
DeprecatedNamespace: testNS,
URI: &apis.URL{
Path: "foo",
},
},
wantURI: addressableDNSWithPathAndTrailingSlash + "foo",
}, "[apiVersion, kind, name] ends with path and trailing slash and relative URI with leading slash ": {
objects: []runtime.Object{
addressableWithPathAndTrailingSlash(),
},
dest: duckv1beta1.Destination{
DeprecatedKind: addressableKind,
DeprecatedName: addressableName,
DeprecatedAPIVersion: addressableAPIVersion,
DeprecatedNamespace: testNS,
URI: &apis.URL{
Path: "/foo",
},
},
wantURI: addressableDNS + "/foo",
}, "[apiVersion, kind, name] ends with path and no trailing slash and relative URI without leading slash ": {
objects: []runtime.Object{
addressableWithPathAndNoTrailingSlash(),
},
dest: duckv1beta1.Destination{
DeprecatedKind: addressableKind,
DeprecatedName: addressableName,
DeprecatedAPIVersion: addressableAPIVersion,
DeprecatedNamespace: testNS,
URI: &apis.URL{
Path: "foo",
},
},
wantURI: addressableDNS + "/foo",
}, "[apiVersion, kind, name] ends with path and no trailing slash and relative URI with leading slash ": {
objects: []runtime.Object{
addressableWithPathAndNoTrailingSlash(),
},
dest: duckv1beta1.Destination{
DeprecatedKind: addressableKind,
DeprecatedName: addressableName,
DeprecatedAPIVersion: addressableAPIVersion,
DeprecatedNamespace: testNS,
URI: &apis.URL{
Path: "/foo",
},
},
wantURI: addressableDNS + "/foo",
}, "[apiVersion, kind, name] with URI which is absolute URL": {
objects: []runtime.Object{
getAddressable(),
},
dest: duckv1beta1.Destination{
DeprecatedKind: addressableKind,
DeprecatedName: addressableName,
DeprecatedAPIVersion: addressableAPIVersion,
DeprecatedNamespace: testNS,
URI: &apis.URL{
Scheme: "http",
Host: "example.com",
Path: "/foo",
},
},
wantErr: "absolute URI is not allowed when Ref or [apiVersion, kind, name] exists",
},
"nil url": {
objects: []runtime.Object{
addressableNilURL(),
},
dest: duckv1beta1.Destination{Ref: unaddressableRef()},
wantErr: fmt.Sprintf("URL missing in address of %s", unaddressableKnativeRef()),
},
"nil address": {
objects: []runtime.Object{
addressableNilAddress(),
},
dest: duckv1beta1.Destination{Ref: unaddressableRef()},
wantErr: fmt.Sprintf("address not set for %s", unaddressableKnativeRef()),
}, "missing host": {
objects: []runtime.Object{
addressableNoHostURL(),
},
dest: duckv1beta1.Destination{Ref: unaddressableRef()},
wantErr: fmt.Sprintf("hostname missing in address of %s", unaddressableKnativeRef()),
}, "missing status": {
objects: []runtime.Object{
addressableNoStatus(),
},
dest: duckv1beta1.Destination{Ref: unaddressableRef()},
wantErr: fmt.Sprintf("address not set for %s", unaddressableKnativeRef()),
}, "notFound": {
dest: duckv1beta1.Destination{Ref: unaddressableRef()},
wantErr: fmt.Sprintf("failed to get object %s/%s: %s %q not found", testNS, unaddressableName, unaddressableResource, unaddressableName),
},
}
for n, tc := range tests {
t.Run(n, func(t *testing.T) {
ctx, _ := fakedynamicclient.With(context.Background(), scheme.Scheme, tc.objects...)
ctx = addressable.WithDuck(ctx)
r := resolver.NewURIResolverFromTracker(ctx, tracker.New(func(types.NamespacedName) {}, 0))
// Run it twice since this should be idempotent. URI Resolver should
// not modify the cache's copy.
_, _ = r.URIFromDestination(ctx, tc.dest, getAddressable())
uri, gotErr := r.URIFromDestination(ctx, tc.dest, getAddressable())
if gotErr != nil {
if tc.wantErr != "" {
if got, want := gotErr.Error(), tc.wantErr; got != want {
t.Errorf("Unexpected error (-want, +got) =\n%s", cmp.Diff(want, got))
}
} else {
t.Error("Unexpected error:", gotErr)
}
return
}
if got, want := uri, tc.wantURI; got != want {
t.Errorf("Unexpected object (-want, +got) =\n%s", cmp.Diff(got, want))
}
})
}
}
func TestGetURIDestinationV1(t *testing.T) {
tests := map[string]struct {
objects []runtime.Object
dest duckv1.Destination
customResolvers []resolver.RefResolverFunc
wantURI string
wantErr string
}{
"nil everything": {
wantErr: "destination missing Ref and URI, expected at least one",
}, "Happy URI with path": {
dest: duckv1.Destination{
URI: &apis.URL{
Scheme: "http",
Host: "example.com",
Path: "/foo",
},
},
wantURI: "http://example.com/foo",
}, "URI is not absolute URL": {
dest: duckv1.Destination{
URI: &apis.URL{
Host: "example.com",
},
},
wantErr: fmt.Sprintf("URI is not absolute (both scheme and host should be non-empty): %q", "//example.com"),
}, "URI with no host": {
dest: duckv1.Destination{
URI: &apis.URL{
Scheme: "http",
},
},
wantErr: fmt.Sprintf("URI is not absolute (both scheme and host should be non-empty): %q", "http:"),
},
"happy ref": {
objects: []runtime.Object{
getAddressable(),
},
dest: duckv1.Destination{Ref: addressableKnativeRef()},
wantURI: addressableDNS,
}, "happy ref to k8s service": {
objects: []runtime.Object{
getAddressableFromKRef(k8sServiceRef()),
},
dest: duckv1.Destination{Ref: k8sServiceRef()},
wantURI: "http://testsink.testnamespace.svc.cluster.local",
}, "ref with relative uri": {
objects: []runtime.Object{
getAddressable(),
},
dest: duckv1.Destination{
Ref: addressableKnativeRef(),
URI: &apis.URL{
Path: "/foo",
},
},
wantURI: addressableDNS + "/foo",
}, "ref with relative URI without leading slash": {
objects: []runtime.Object{
getAddressable(),
},
dest: duckv1.Destination{
Ref: addressableKnativeRef(),
URI: &apis.URL{
Path: "foo",
},
},
wantURI: addressableDNS + "/foo",
}, "ref ends with path and trailing slash and relative URI without leading slash ": {
objects: []runtime.Object{
addressableWithPathAndTrailingSlash(),
},
dest: duckv1.Destination{
Ref: addressableKnativeRef(),
URI: &apis.URL{
Path: "foo",
},
},
wantURI: addressableDNSWithPathAndTrailingSlash + "foo",
}, "ref ends with path and trailing slash and relative URI with leading slash ": {
objects: []runtime.Object{
addressableWithPathAndTrailingSlash(),
},
dest: duckv1.Destination{
Ref: addressableKnativeRef(),
URI: &apis.URL{
Path: "/foo",
},
},
wantURI: addressableDNS + "/foo",
}, "ref ends with path and no trailing slash and relative URI without leading slash ": {
objects: []runtime.Object{
addressableWithPathAndNoTrailingSlash(),
},
dest: duckv1.Destination{
Ref: addressableKnativeRef(),
URI: &apis.URL{
Path: "foo",
},
},
wantURI: addressableDNS + "/foo",
}, "ref ends with path and no trailing slash and relative URI with leading slash ": {
objects: []runtime.Object{
addressableWithPathAndNoTrailingSlash(),
},
dest: duckv1.Destination{
Ref: addressableKnativeRef(),
URI: &apis.URL{
Path: "/foo",
},
},
wantURI: addressableDNS + "/foo",
}, "ref with URI which is absolute URL": {
objects: []runtime.Object{
getAddressable(),
},
dest: duckv1.Destination{
Ref: addressableKnativeRef(),
URI: &apis.URL{
Scheme: "http",
Host: "example.com",
Path: "/foo",
},
},
wantErr: "absolute URI is not allowed when Ref or [apiVersion, kind, name] exists",
},
"nil url": {
objects: []runtime.Object{
addressableNilURL(),
},
dest: duckv1.Destination{Ref: unaddressableKnativeRef()},
wantErr: fmt.Sprintf("URL missing in address of %s", unaddressableKnativeRef()),
},
"nil address": {
objects: []runtime.Object{
addressableNilAddress(),
},
dest: duckv1.Destination{Ref: unaddressableKnativeRef()},
wantErr: fmt.Sprintf("address not set for %s", unaddressableKnativeRef()),
}, "missing host": {
objects: []runtime.Object{
addressableNoHostURL(),
},
dest: duckv1.Destination{Ref: unaddressableKnativeRef()},
wantErr: fmt.Sprintf("hostname missing in address of %s", unaddressableKnativeRef()),
}, "missing status": {
objects: []runtime.Object{
addressableNoStatus(),
},
dest: duckv1.Destination{Ref: unaddressableKnativeRef()},
wantErr: fmt.Sprintf("address not set for %s", unaddressableKnativeRef()),
}, "notFound": {
dest: duckv1.Destination{Ref: unaddressableKnativeRef()},
wantErr: fmt.Sprintf("failed to get object %s/%s: %s %q not found", testNS, unaddressableName, unaddressableResource, unaddressableName),
}, "notFound k8s service": {
dest: duckv1.Destination{Ref: k8sServiceRef()},
wantErr: fmt.Sprintf("failed to get object %s/%s: services %q not found", testNS, addressableName, addressableName),
}, "with sample resolver": {
dest: duckv1.Destination{Ref: k8sServiceRef()},
customResolvers: []resolver.RefResolverFunc{sampleURIResolver},
wantURI: "ref://" + addressableName + ".Service.v1",
}, "happy ref with sample resolver": {
objects: []runtime.Object{
getAddressable(),
},
dest: duckv1.Destination{Ref: addressableKnativeRef()},
wantURI: addressableDNS,
customResolvers: []resolver.RefResolverFunc{sampleURIResolver},
}, "unaddressable with sample resolver": {
dest: duckv1.Destination{Ref: unaddressableKnativeRef()},
customResolvers: []resolver.RefResolverFunc{sampleURIResolver},
wantErr: "cannot be referenced",
}, "happy with two sample resolvers, first one passes": {
dest: duckv1.Destination{Ref: k8sServiceRef()},
customResolvers: []resolver.RefResolverFunc{sampleURIResolver, noopURIResolver},
wantURI: "ref://" + addressableName + ".Service.v1",
}, "happy with two sample resolvers, second one passes": {
dest: duckv1.Destination{Ref: k8sServiceRef()},
customResolvers: []resolver.RefResolverFunc{noopURIResolver, sampleURIResolver},
wantURI: "ref://" + addressableName + ".Service.v1",
},
}
for n, tc := range tests {
t.Run(n, func(t *testing.T) {
ctx, _ := fakedynamicclient.With(context.Background(), scheme.Scheme, tc.objects...)
ctx = addressable.WithDuck(ctx)
r := resolver.NewURIResolverFromTracker(ctx, tracker.New(func(types.NamespacedName) {}, 0), tc.customResolvers...)
// Run it twice since this should be idempotent. URI Resolver should
// not modify the cache's copy.
_, _ = r.URIFromDestinationV1(ctx, tc.dest, getAddressable())
uri, gotErr := r.URIFromDestinationV1(ctx, tc.dest, getAddressable())
if gotErr != nil {
if tc.wantErr != "" {
if got, want := gotErr.Error(), tc.wantErr; got != want {
t.Errorf("Unexpected error (-want, +got) =\n%s", cmp.Diff(want, got))
}
} else {
t.Error("Unexpected error:", gotErr)
}
}
if got, want := uri.String(), tc.wantURI; got != want {
t.Errorf("Unexpected object (-want, +got) =\n%s", cmp.Diff(want, got))
}
})
}
}
func TestURIFromObjectReferenceErrors(t *testing.T) {
tests := map[string]struct {
objects []runtime.Object
ref *corev1.ObjectReference
wantErr string
}{"nil": {
wantErr: "ref is nil",
}, "fail tracker with bad object": {
ref: invalidObjectRef(),
wantErr: `failed to track reference duck.knative.dev/v1, Resource=sinks -bad/testsink: invalid Reference:
Namespace: a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')`,
}, "fail get": {
ref: addressableRef(),
wantErr: `failed to get object testnamespace/testsink: sinks.duck.knative.dev "testsink" not found`,
}}
for n, tc := range tests {
t.Run(n, func(t *testing.T) {
ctx, _ := fakedynamicclient.With(context.Background(), scheme.Scheme, tc.objects...)
ctx = addressable.WithDuck(ctx)
r := resolver.NewURIResolverFromTracker(ctx, tracker.New(func(types.NamespacedName) {}, 0))
// Run it twice since this should be idempotent. URI Resolver should
// not modify the cache's copy.
_, err1 := r.URIFromObjectReference(ctx, tc.ref, getAddressable())
_, err2 := r.URIFromObjectReference(ctx, tc.ref, getAddressable())
if err2 == nil {
t.Fatal("Expected failure")
}
if got, want := err2.Error(), tc.wantErr; got != want {
t.Errorf("Unexpected error (-want, +got) =\n%s", cmp.Diff(want, got))
}
if err1.Error() != err2.Error() {
t.Errorf("Idempotency fail: first err = %+v, second err = %+v", err1, err2)
}
})
}
}
func TestAddressableFromDestinationV1(t *testing.T) {
tests := map[string]struct {
objects []runtime.Object
dest duckv1.Destination
addr *unstructured.Unstructured
customResolvers []resolver.RefResolverFunc
wantAddress *duckv1.Addressable
wantErr string
}{"address and addresses are not set": {
dest: duckv1.Destination{
Ref: addressableKnativeRef(),
URI: &apis.URL{
Path: "/foo",
},
},
objects: []runtime.Object{
addressableNoAddresses(),
},
addr: addressableNoAddresses(),
wantErr: fmt.Sprintf("address not set for %s", addressableKnativeRef()),
}, "ref.address is set on destination and is present in target addressable addresses": {
dest: duckv1.Destination{
Ref: addressableKnativeRefWithAddress(),
URI: &apis.URL{
Path: "/foo",
},
},
objects: []runtime.Object{
addressableWithAddresses(),
},
wantAddress: addrWithName(),
addr: addressableWithAddresses(),
}, "ref.address is set on destination and is NOT present in target addressable addresses": {
dest: duckv1.Destination{
Ref: addressableKnativeRefWithAddress(),
URI: &apis.URL{
Path: "/foo",
},
},
objects: []runtime.Object{
addressableWithDifferentAddresses(),
},
addr: addressableWithDifferentAddresses(),
wantErr: fmt.Sprintf("address with name %q not found for %s", *addressableKnativeRefWithAddress().Address, addressableKnativeRefWithAddress()),
}, "address and addresses are set but no destination.ref.address is set": {
dest: duckv1.Destination{
Ref: addressableKnativeRef(),
URI: &apis.URL{
Path: "/foo",
},
},
objects: []runtime.Object{
addressableWithAddresses(),
},
addr: addressableWithAddresses(),
wantAddress: addrWithName(),
}, "only address is set and no destination.ref.address is set": {
dest: duckv1.Destination{
Ref: addressableKnativeRef(),
URI: &apis.URL{
Path: "/foo",
},
},
objects: []runtime.Object{
addressableWithAddressOnly(),
},
addr: addressableWithAddressOnly(),
wantAddress: addrWithName(),
}}
for n, tc := range tests {
t.Run(n, func(t *testing.T) {
ctx, _ := fakedynamicclient.With(context.Background(), scheme.Scheme, tc.objects...)
ctx = addressable.WithDuck(ctx)
r := resolver.NewURIResolverFromTracker(ctx, tracker.New(func(types.NamespacedName) {}, 0), tc.customResolvers...)
// Run it twice since this should be idempotent. URI Resolver should
// not modify the cache's copy.
_, _ = r.AddressableFromDestinationV1(ctx, tc.dest, tc.addr)
addr, gotErr := r.AddressableFromDestinationV1(ctx, tc.dest, tc.addr)
if gotErr != nil {
if tc.wantErr != "" {
if got, want := gotErr.Error(), tc.wantErr; got != want {
t.Errorf("Unexpected error (-want, +got) =\n%s", cmp.Diff(want, got))
}
} else {
t.Error("Unexpected error:", gotErr)
}
return
}
if !equality.Semantic.DeepEqual(addr, tc.wantAddress) {
t.Errorf("Unexpected object (-want, +got) =\n%s", cmp.Diff(addr, tc.wantAddress))
}
})
}
}
func TestAddressableFromDestinationScheme(t *testing.T) {
cert := CACert
tests := map[string]struct {
objects []runtime.Object
dest duckv1.Destination
addr *unstructured.Unstructured
customResolvers []resolver.RefResolverFunc
wantScheme string
wantErr string
}{"destination is a raw k8s serving and there is CACerts set ": {
dest: duckv1.Destination{
Ref: k8sServiceRef(),
URI: &apis.URL{
Path: "/foo",
},
CACerts: &cert,
},
objects: []runtime.Object{
getAddressableFromKRef(k8sServiceRef()),
},
wantScheme: "https",
addr: addressableWithAddresses(),
}}
for n, tc := range tests {
t.Run(n, func(t *testing.T) {
ctx, _ := fakedynamicclient.With(context.Background(), scheme.Scheme, tc.objects...)
ctx = addressable.WithDuck(ctx)
r := resolver.NewURIResolverFromTracker(ctx, tracker.New(func(types.NamespacedName) {}, 0), tc.customResolvers...)
// Run it twice since this should be idempotent. URI Resolver should
// not modify the cache's copy.
_, _ = r.AddressableFromDestinationV1(ctx, tc.dest, tc.addr)
addr, gotErr := r.AddressableFromDestinationV1(ctx, tc.dest, tc.addr)
if gotErr != nil {
if tc.wantErr != "" {
if got, want := gotErr.Error(), tc.wantErr; got != want {
t.Errorf("Unexpected error (-want, +got) =\n%s", cmp.Diff(want, got))
}
} else {
t.Error("Unexpected error:", gotErr)
}
}
if got, want := addr.URL.Scheme, tc.wantScheme; got != want {
t.Errorf("Unexpected object (-want, +got) =\n%s", cmp.Diff(want, got))
}
})
}
}
func TestAddressableFromDestinationV1CACerts(t *testing.T) {
certDestination := "CA CERT FOR DESTINATION"
tests := map[string]struct {
objects []runtime.Object
dest duckv1.Destination
addr *unstructured.Unstructured
customResolvers []resolver.RefResolverFunc
wantCert string
wantErr string
}{"CACerts is set on the target addressable": {
dest: duckv1.Destination{
Ref: addressableKnativeRef(),
URI: &apis.URL{
Path: "/foo",
},
},
objects: []runtime.Object{
addressableWithCACert(),
},
addr: addressableWithCACert(),
wantErr: fmt.Sprintf("address with name %q not found for %+v", *addressableKnativeRefWithAddress().Address, addressableKnativeRefWithAddress()),
wantCert: CACert,
}, "CACerts is not set on the target addressable but it is set on the destination": {
dest: duckv1.Destination{
Ref: addressableKnativeRef(),
URI: &apis.URL{
Path: "/foo",
},
CACerts: &certDestination,
},
objects: []runtime.Object{
addressableWithAddresses(),
},
addr: addressableWithAddresses(),
wantCert: certDestination,
}, "CACerts is set on the target addressable and it is set on the destination": {
dest: duckv1.Destination{
Ref: addressableKnativeRef(),
URI: &apis.URL{
Path: "/foo",
},
CACerts: &certDestination,
},
objects: []runtime.Object{
addressableWithCACert(),
},
addr: addressableWithCACert(),
wantCert: certDestination,
}}
for n, tc := range tests {
t.Run(n, func(t *testing.T) {
ctx, _ := fakedynamicclient.With(context.Background(), scheme.Scheme, tc.objects...)
ctx = addressable.WithDuck(ctx)
r := resolver.NewURIResolverFromTracker(ctx, tracker.New(func(types.NamespacedName) {}, 0), tc.customResolvers...)
// Run it twice since this should be idempotent. URI Resolver should
// not modify the cache's copy.
_, _ = r.AddressableFromDestinationV1(ctx, tc.dest, tc.addr)
addr, gotErr := r.AddressableFromDestinationV1(ctx, tc.dest, tc.addr)
if gotErr != nil {
if tc.wantErr != "" {
if got, want := gotErr.Error(), tc.wantErr; got != want {
t.Errorf("Unexpected error (-want, +got) =\n%s", cmp.Diff(want, got))
}
} else {
t.Error("Unexpected error:", gotErr)
}
}
if got, want := *addr.CACerts, tc.wantCert; got != want {
t.Errorf("Unexpected object (-want, +got) =\n%s", cmp.Diff(want, got))
}
})
}
}
func TestAddressableFromDestinationV1Audience(t *testing.T) {
destinationAudience := "destination-audience"
tests := map[string]struct {
objects []runtime.Object
dest duckv1.Destination
addr *unstructured.Unstructured
customResolvers []resolver.RefResolverFunc
wantAudience string
wantErr string
}{"Audience is set on destination": {
dest: duckv1.Destination{
Ref: addressableKnativeRef(),
URI: &apis.URL{
Path: "/foo",
},
Audience: &destinationAudience,
},
objects: []runtime.Object{
addressableWithAddresses(),
},
addr: addressableWithAddresses(),
wantAudience: destinationAudience,
}, "Audience is set on the target addressable but not set on the destination": {
dest: duckv1.Destination{
Ref: addressableKnativeRef(),
URI: &apis.URL{
Path: "/foo",
},
},
objects: []runtime.Object{
addressableWithAudience(),
},
addr: addressableWithAudience(),
wantAudience: addressableAudience,
}, "Audience is set on the target addressable and set on the destination (destination should have priority)": {
dest: duckv1.Destination{
Ref: addressableKnativeRef(),
URI: &apis.URL{
Path: "/foo",
},
Audience: &destinationAudience,
},
objects: []runtime.Object{
addressableWithAudience(),
},
addr: addressableWithAudience(),
wantAudience: destinationAudience,
}}
for n, tc := range tests {
t.Run(n, func(t *testing.T) {
ctx, _ := fakedynamicclient.With(context.Background(), scheme.Scheme, tc.objects...)
ctx = addressable.WithDuck(ctx)
r := resolver.NewURIResolverFromTracker(ctx, tracker.New(func(types.NamespacedName) {}, 0), tc.customResolvers...)
// Run it twice since this should be idempotent. URI Resolver should
// not modify the cache's copy.
_, _ = r.AddressableFromDestinationV1(ctx, tc.dest, tc.addr)
addr, gotErr := r.AddressableFromDestinationV1(ctx, tc.dest, tc.addr)
if gotErr != nil {
if tc.wantErr != "" {
if got, want := gotErr.Error(), tc.wantErr; got != want {
t.Errorf("Unexpected error (-want, +got) =\n%s", cmp.Diff(want, got))
}
} else {
t.Error("Unexpected error:", gotErr)
}
}
if got, want := *addr.Audience, tc.wantAudience; got != want {
t.Errorf("Unexpected object (-want, +got) =\n%s", cmp.Diff(want, got))
}
})
}
}
func getAddressable() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": addressableAPIVersion,
"kind": addressableKind,
"metadata": map[string]interface{}{
"namespace": testNS,
"name": addressableName,
},
"status": map[string]interface{}{
"address": map[string]interface{}{
"url": addressableDNS,
},
},
},
}
}
func addressableWithPathAndTrailingSlash() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": addressableAPIVersion,
"kind": addressableKind,
"metadata": map[string]interface{}{
"namespace": testNS,
"name": addressableName,
},
"status": map[string]interface{}{
"address": map[string]interface{}{
"url": addressableDNSWithPathAndTrailingSlash,
},
},
},
}
}
func addressableWithPathAndNoTrailingSlash() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": addressableAPIVersion,
"kind": addressableKind,
"metadata": map[string]interface{}{
"namespace": testNS,
"name": addressableName,
},
"status": map[string]interface{}{
"address": map[string]interface{}{
"url": addressableDNSWithPathAndNoTrailingSlash,
},
},
},
}
}
func addressableNoStatus() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": unaddressableAPIVersion,
"kind": unaddressableKind,
"metadata": map[string]interface{}{
"namespace": testNS,
"name": unaddressableName,
},
},
}
}
func addressableNilAddress() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": unaddressableAPIVersion,
"kind": unaddressableKind,
"metadata": map[string]interface{}{
"namespace": testNS,
"name": unaddressableName,
},
"status": map[string]interface{}{
"address": map[string]interface{}(nil),
},
},
}
}
func addressableNilURL() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": unaddressableAPIVersion,
"kind": unaddressableKind,
"metadata": map[string]interface{}{
"namespace": testNS,
"name": unaddressableName,
},
"status": map[string]interface{}{
"address": map[string]interface{}{
"url": nil,
},
},
},
}
}
func addressableNoHostURL() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": unaddressableAPIVersion,
"kind": unaddressableKind,
"metadata": map[string]interface{}{
"namespace": testNS,
"name": unaddressableName,
},
"status": map[string]interface{}{
"address": map[string]interface{}{
"url": "http://",
},
},
},
}
}
func invalidObjectRef() *corev1.ObjectReference {
return &corev1.ObjectReference{
Kind: addressableKind,
Name: addressableName,
APIVersion: addressableAPIVersion,
Namespace: "-bad",
}
}
func k8sServiceRef() *duckv1.KReference {
return &duckv1.KReference{
Kind: "Service",
Name: addressableName,
APIVersion: "v1",
Namespace: testNS,
}
}
func addressableKnativeRef() *duckv1.KReference {
return &duckv1.KReference{
Kind: addressableKind,
Name: addressableName,
APIVersion: addressableAPIVersion,
Namespace: testNS,
}
}
func addressableKnativeRefWithAddress() *duckv1.KReference {
address := addressableName
return &duckv1.KReference{
Kind: addressableKind,
Name: addressableName,
APIVersion: addressableAPIVersion,
Namespace: testNS,
Address: &address,
}
}
func addressableRef() *corev1.ObjectReference {
return &corev1.ObjectReference{
Kind: addressableKind,
Name: addressableName,
APIVersion: addressableAPIVersion,
Namespace: testNS,
}
}
func unaddressableKnativeRef() *duckv1.KReference {
return &duckv1.KReference{
Kind: unaddressableKind,
Name: unaddressableName,
APIVersion: unaddressableAPIVersion,
Namespace: testNS,
}
}
func unaddressableRef() *corev1.ObjectReference {
return &corev1.ObjectReference{
Kind: unaddressableKind,
Name: unaddressableName,
APIVersion: unaddressableAPIVersion,
Namespace: testNS,
}
}
func getAddressableFromKRef(ref *duckv1.KReference) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": ref.APIVersion,
"kind": ref.Kind,
"metadata": map[string]interface{}{
"namespace": ref.Namespace,
"name": ref.Name,
},
"status": map[string]interface{}{
"address": map[string]interface{}{
"url": addressableDNS,
"name": addressableDNS,
},
},
},
}
}
func addressableNoAddresses() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": addressableAPIVersion,
"kind": addressableKind,
"metadata": map[string]interface{}{
"namespace": testNS,
"name": addressableName,
},
"status": map[string]interface{}{
"address": map[string]interface{}(nil),
"addresses": []interface{}{},
},
},
}
}
func addressableWithAddresses() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": addressableAPIVersion,
"kind": addressableKind,
"metadata": map[string]interface{}{
"namespace": testNS,
"name": addressableName,
},
"status": map[string]interface{}{
"address": map[string]interface{}{
"url": addressableDNS,
"name": addressableName,
},
"addresses": []interface{}{
map[string]interface{}{
"url": addressableDNS,
"name": addressableName,
}, map[string]interface{}{
"url": addressableDNS1,
"name": addressableName1,
}, map[string]interface{}{
"url": addressableDNS2,
"name": addressableName2,
}, map[string]interface{}{
"url": addressableDNS3,
"name": addressableName3,
},
},
},
},
}
}
func addressableWithAddressOnly() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": addressableAPIVersion,
"kind": addressableKind,
"metadata": map[string]interface{}{
"namespace": testNS,
"name": addressableName,
},
"status": map[string]interface{}{
"address": map[string]interface{}{
"url": addressableDNS,
"name": addressableName,
},
"addresses": []interface{}{},
},
},
}
}
func addressableWithDifferentAddresses() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": addressableAPIVersion,
"kind": addressableKind,
"metadata": map[string]interface{}{
"namespace": testNS,
"name": addressableName,
},
"status": map[string]interface{}{
"address": map[string]interface{}{
"url": addressableDNS,
"name": addressableName,
},
"addresses": []interface{}{
map[string]interface{}{
"url": addressableDNS1,
"name": addressableName1,
}, map[string]interface{}{
"url": addressableDNS2,
"name": addressableName2,
},
},
},
},
}
}
func addressableWithCACert() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": addressableAPIVersion,
"kind": addressableKind,
"metadata": map[string]interface{}{
"namespace": testNS,
"name": addressableName,
},
"status": map[string]interface{}{
"address": map[string]interface{}{
"url": addressableDNS,
"name": addressableName,
"CACerts": CACert,
},
},
},
}
}
func addressableWithAudience() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": addressableAPIVersion,
"kind": addressableKind,
"metadata": map[string]interface{}{
"namespace": testNS,
"name": addressableName,
},
"status": map[string]interface{}{
"address": map[string]interface{}{
"url": addressableDNS,
"name": addressableName,
"audience": addressableAudience,
},
},
},
}
}
func sampleURIResolver(ctx context.Context, ref *corev1.ObjectReference) (bool, *apis.URL, error) {
if ref.Kind == "Service" {
parsed, err := apis.ParseURL(fmt.Sprintf("ref://%s.%s.%s", ref.Name, ref.Kind, ref.APIVersion))
return true, parsed, err
}
if ref.Kind == unaddressableKind {
return true, nil, errors.New("cannot be referenced")
}
return false, nil, nil
}
func noopURIResolver(ctx context.Context, ref *corev1.ObjectReference) (bool, *apis.URL, error) {
return false, nil, nil
}
func addrWithName() *duckv1.Addressable {
addrName := addressableName
return &duckv1.Addressable{
Name: &addrName,
URL: &apis.URL{
Host: "addressable.sink.svc.cluster.local",
Scheme: "http",
Path: "/foo",
},
}
}