diff --git a/docs/sources/sources.md b/docs/sources/sources.md index b156624918..5f262bb404 100644 --- a/docs/sources/sources.md +++ b/docs/sources/sources.md @@ -2,7 +2,7 @@ | Source | Resources | annotation-filter | label-filter | |---------------------------------|-------------------------------------------------------------------------------|-------------------|--------------| -| ambassador-host | Host.getambassador.io | | | +| ambassador-host | Host.getambassador.io | Yes | Yes | | connector | | | | | contour-httpproxy | HttpProxy.projectcontour.io | Yes | | | cloudfoundry | | | | diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 46d46ad978..f40e23ee7e 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -438,7 +438,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("openshift-router-name", "if source is openshift-route then you can pass the ingress controller name. Based on this name external-dns will select the respective router from the route status and map that routerCanonicalHostname to the route host while creating a CNAME record.").StringVar(&cfg.OCPRouterName) app.Flag("namespace", "Limit resources queried for endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace) app.Flag("annotation-filter", "Filter resources queried for endpoints by annotation, using label selector semantics").Default(defaultConfig.AnnotationFilter).StringVar(&cfg.AnnotationFilter) - app.Flag("label-filter", "Filter resources queried for endpoints by label selector; currently supported by source types crd, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, ingress, node, openshift-route, and service").Default(defaultConfig.LabelFilter).StringVar(&cfg.LabelFilter) + app.Flag("label-filter", "Filter resources queried for endpoints by label selector; currently supported by source types crd, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, ingress, node, openshift-route, service and ambassador-host").Default(defaultConfig.LabelFilter).StringVar(&cfg.LabelFilter) app.Flag("ingress-class", "Require an Ingress to have this class name (defaults to any class; specify multiple times to allow more than one class)").StringsVar(&cfg.IngressClassNames) app.Flag("fqdn-template", "A templated string that's used to generate DNS names from sources that don't define a hostname themselves, or to add a hostname suffix when paired with the fake source (optional). Accepts comma separated list for multiple global FQDN.").Default(defaultConfig.FQDNTemplate).StringVar(&cfg.FQDNTemplate) app.Flag("combine-fqdn-annotation", "Combine FQDN template and Annotations instead of overwriting").BoolVar(&cfg.CombineFQDNAndAnnotation) diff --git a/source/ambassador_host.go b/source/ambassador_host.go index 634b22073b..9be6bc9da4 100644 --- a/source/ambassador_host.go +++ b/source/ambassador_host.go @@ -57,8 +57,10 @@ type ambassadorHostSource struct { dynamicKubeClient dynamic.Interface kubeClient kubernetes.Interface namespace string + annotationFilter string ambassadorHostInformer informers.GenericInformer unstructuredConverter *unstructuredConverter + labelSelector labels.Selector } // NewAmbassadorHostSource creates a new ambassadorHostSource with the given config. @@ -67,6 +69,8 @@ func NewAmbassadorHostSource( dynamicKubeClient dynamic.Interface, kubeClient kubernetes.Interface, namespace string, + annotationFilter string, + labelSelector labels.Selector, ) (Source, error) { var err error @@ -85,6 +89,7 @@ func NewAmbassadorHostSource( informerFactory.Start(ctx.Done()) + // wait for the local cache to be populated. if err := waitForDynamicCacheSync(context.Background(), informerFactory); err != nil { return nil, err } @@ -98,20 +103,23 @@ func NewAmbassadorHostSource( dynamicKubeClient: dynamicKubeClient, kubeClient: kubeClient, namespace: namespace, + annotationFilter: annotationFilter, ambassadorHostInformer: ambassadorHostInformer, unstructuredConverter: uc, + labelSelector: labelSelector, }, nil } // Endpoints returns endpoint objects for each host-target combination that should be processed. // Retrieves all Hosts in the source's namespace(s). func (sc *ambassadorHostSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { - hosts, err := sc.ambassadorHostInformer.Lister().ByNamespace(sc.namespace).List(labels.Everything()) + hosts, err := sc.ambassadorHostInformer.Lister().ByNamespace(sc.namespace).List(sc.labelSelector) if err != nil { return nil, err } - endpoints := []*endpoint.Endpoint{} + // Get a list of Ambassador Host resources + ambassadorHosts := []*ambassador.Host{} for _, hostObj := range hosts { unstructuredHost, ok := hostObj.(*unstructured.Unstructured) if !ok { @@ -123,7 +131,18 @@ func (sc *ambassadorHostSource) Endpoints(ctx context.Context) ([]*endpoint.Endp if err != nil { return nil, err } + ambassadorHosts = append(ambassadorHosts, host) + } + + // Filter Ambassador Hosts + ambassadorHosts, err = sc.filterByAnnotations(ambassadorHosts) + if err != nil { + return nil, errors.Wrap(err, "failed to filter Ambassador Hosts by annotation") + } + endpoints := []*endpoint.Endpoint{} + + for _, host := range ambassadorHosts { fullname := fmt.Sprintf("%s/%s", host.Namespace, host.Name) // look for the "exernal-dns.ambassador-service" annotation. If it is not there then just ignore this `Host` @@ -272,3 +291,35 @@ func newUnstructuredConverter() (*unstructuredConverter, error) { return uc, nil } + +// Filter a list of Ambassador Host Resources to only return the ones that +// contain the required External-DNS annotation filter +func (sc *ambassadorHostSource) filterByAnnotations(ambassadorHosts []*ambassador.Host) ([]*ambassador.Host, error) { + // External-DNS Annotation Filter + labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter) + if err != nil { + return nil, err + } + + selector, err := metav1.LabelSelectorAsSelector(labelSelector) + if err != nil { + return nil, err + } + + // empty filter returns original list of Ambassador Hosts + if selector.Empty() { + return ambassadorHosts, nil + } + + // Return a filtered list of Ambassador Hosts + filteredList := []*ambassador.Host{} + for _, host := range ambassadorHosts { + annotations := labels.Set(host.Annotations) + // include Ambassador Host if its annotations match the annotation filter + if selector.Matches(annotations) { + filteredList = append(filteredList, host) + } + } + + return filteredList, nil +} diff --git a/source/ambassador_host_test.go b/source/ambassador_host_test.go index 9702eb1196..28c17740e8 100644 --- a/source/ambassador_host_test.go +++ b/source/ambassador_host_test.go @@ -17,19 +17,26 @@ limitations under the License. package source import ( + "context" "testing" ambassador "github.com/datawire/ambassador/pkg/api/getambassador.io/v2" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - "golang.org/x/net/context" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" fakeDynamic "k8s.io/client-go/dynamic/fake" - fakeKube "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/kubernetes/fake" + "sigs.k8s.io/external-dns/endpoint" ) +// This is a compile-time validation that ambassadorHostSource is a Source. +var _ Source = &ambassadorHostSource{} + type AmbassadorSuite struct { suite.Suite } @@ -37,6 +44,7 @@ type AmbassadorSuite struct { func TestAmbassadorSource(t *testing.T) { suite.Run(t, new(AmbassadorSuite)) t.Run("Interface", testAmbassadorSourceImplementsSource) + t.Run("Endpoints", testAmbassadorSourceEndpoints) } // testAmbassadorSourceImplementsSource tests that ambassadorHostSource is a valid Source. @@ -45,7 +53,7 @@ func testAmbassadorSourceImplementsSource(t *testing.T) { } func TestAmbassadorHostSource(t *testing.T) { - fakeKubernetesClient := fakeKube.NewSimpleClientset() + fakeKubernetesClient := fake.NewSimpleClientset() ambassadorScheme := runtime.NewScheme() @@ -57,19 +65,23 @@ func TestAmbassadorHostSource(t *testing.T) { namespace := "test" + annotationFilter := "" + + labelSelector := labels.Everything() + host, err := createAmbassadorHost("test-host", "test-service") if err != nil { t.Fatalf("could not create host resource: %v", err) } { - _, err := fakeDynamicClient.Resource(ambHostGVR).Namespace(namespace).Create(ctx, host, v1.CreateOptions{}) + _, err := fakeDynamicClient.Resource(ambHostGVR).Namespace(namespace).Create(ctx, host, metav1.CreateOptions{}) if err != nil { t.Fatalf("could not create host: %v", err) } } - ambassadorSource, err := NewAmbassadorHostSource(ctx, fakeDynamicClient, fakeKubernetesClient, namespace) + ambassadorSource, err := NewAmbassadorHostSource(ctx, fakeDynamicClient, fakeKubernetesClient, namespace, annotationFilter, labelSelector) if err != nil { t.Fatalf("could not create ambassador source: %v", err) } @@ -84,7 +96,7 @@ func TestAmbassadorHostSource(t *testing.T) { func createAmbassadorHost(name, ambassadorService string) (*unstructured.Unstructured, error) { host := &ambassador.Host{ - ObjectMeta: v1.ObjectMeta{ + ObjectMeta: metav1.ObjectMeta{ Name: name, Annotations: map[string]string{ ambHostAnnotation: ambassadorService, @@ -101,6 +113,529 @@ func createAmbassadorHost(name, ambassadorService string) (*unstructured.Unstruc return obj, nil } +// testAmbassadorSourceEndpoints tests that various Ambassador Hosts generate the correct endpoints. +func testAmbassadorSourceEndpoints(t *testing.T) { + for _, ti := range []struct { + title string + targetNamespace string + annotationFilter string + loadBalancer fakeAmbassadorLoadBalancerService + ambassadorHostItems []fakeAmbassadorHost + expected []*endpoint.Endpoint + expectError bool + labelSelector labels.Selector + }{ + { + title: "no host", + labelSelector: labels.Everything(), + }, + { + title: "two simple hosts", + labelSelector: labels.Everything(), + loadBalancer: fakeAmbassadorLoadBalancerService{ + ips: []string{"8.8.8.8"}, + hostnames: []string{"lb.com"}, + name: "emissary", + namespace: "emissary-ingress", + }, + ambassadorHostItems: []fakeAmbassadorHost{ + { + name: "fake1", + namespace: "", + hostname: "fake1.org", + annotations: map[string]string{ + "external-dns.ambassador-service": "emissary-ingress/emissary", + }, + }, + { + name: "fake2", + namespace: "", + hostname: "fake2.org", + annotations: map[string]string{ + "external-dns.ambassador-service": "emissary-ingress/emissary", + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "fake1.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, + }, + { + DNSName: "fake1.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, + }, + { + DNSName: "fake2.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, + }, + { + DNSName: "fake2.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, + }, + }, + }, + { + title: "two simple hosts on different namespaces", + labelSelector: labels.Everything(), + loadBalancer: fakeAmbassadorLoadBalancerService{ + ips: []string{"8.8.8.8"}, + hostnames: []string{"lb.com"}, + name: "emissary", + namespace: "emissary-ingress", + }, + ambassadorHostItems: []fakeAmbassadorHost{ + { + name: "fake1", + namespace: "testing1", + hostname: "fake1.org", + annotations: map[string]string{ + "external-dns.ambassador-service": "emissary-ingress/emissary", + }, + }, + { + name: "fake2", + namespace: "testing2", + hostname: "fake2.org", + annotations: map[string]string{ + "external-dns.ambassador-service": "emissary-ingress/emissary", + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "fake1.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, + }, + { + DNSName: "fake1.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, + }, + { + DNSName: "fake2.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, + }, + { + DNSName: "fake2.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, + }, + }, + }, + { + title: "two simple hosts on different namespaces and a target namespace", + targetNamespace: "testing1", + labelSelector: labels.Everything(), + loadBalancer: fakeAmbassadorLoadBalancerService{ + ips: []string{"8.8.8.8"}, + hostnames: []string{"lb.com"}, + name: "emissary", + namespace: "emissary-ingress", + }, + ambassadorHostItems: []fakeAmbassadorHost{ + { + name: "fake1", + namespace: "testing1", + hostname: "fake1.org", + annotations: map[string]string{ + "external-dns.ambassador-service": "emissary-ingress/emissary", + }, + }, + { + name: "fake2", + namespace: "testing2", + hostname: "fake2.org", + annotations: map[string]string{ + "external-dns.ambassador-service": "emissary-ingress/emissary", + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "fake1.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, + }, + { + DNSName: "fake1.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, + }, + }, + }, + { + title: "invalid non matching host ambassador service annotation", + labelSelector: labels.Everything(), + loadBalancer: fakeAmbassadorLoadBalancerService{ + ips: []string{"8.8.8.8"}, + hostnames: []string{"lb.com"}, + name: "emissary", + namespace: "emissary-ingress", + }, + ambassadorHostItems: []fakeAmbassadorHost{ + { + name: "fake1", + namespace: "testing1", + hostname: "fake1.org", + annotations: map[string]string{ + "external-dns.ambassador-service": "emissary-ingress/invalid", + }, + }, + }, + expected: []*endpoint.Endpoint{}, + }, + { + title: "valid matching annotation filter expression", + annotationFilter: "kubernetes.io/ingress.class in (external-ingress)", + labelSelector: labels.Everything(), + loadBalancer: fakeAmbassadorLoadBalancerService{ + ips: []string{"8.8.8.8"}, + hostnames: []string{"lb.com"}, + name: "emissary", + namespace: "emissary-ingress", + }, + ambassadorHostItems: []fakeAmbassadorHost{ + { + name: "fake1", + namespace: "testing1", + hostname: "fake1.org", + annotations: map[string]string{ + "external-dns.ambassador-service": "emissary-ingress/emissary", + "kubernetes.io/ingress.class": "external-ingress", + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "fake1.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, + }, + { + DNSName: "fake1.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, + }, + }, + }, + { + title: "valid non-matching annotation filter expression", + annotationFilter: "kubernetes.io/ingress.class in (external-ingress)", + labelSelector: labels.Everything(), + loadBalancer: fakeAmbassadorLoadBalancerService{ + ips: []string{"8.8.8.8"}, + hostnames: []string{"lb.com"}, + name: "emissary", + namespace: "emissary-ingress", + }, + ambassadorHostItems: []fakeAmbassadorHost{ + { + name: "fake1", + namespace: "testing1", + hostname: "fake1.org", + annotations: map[string]string{ + "external-dns.ambassador-service": "emissary-ingress/emissary", + "kubernetes.io/ingress.class": "internal-ingress", + }, + }, + }, + expected: []*endpoint.Endpoint{}, + }, + { + title: "invalid annotation filter expression", + annotationFilter: "kubernetes.io/ingress.class in (external ingress)", + labelSelector: labels.Everything(), + loadBalancer: fakeAmbassadorLoadBalancerService{ + ips: []string{"8.8.8.8"}, + hostnames: []string{"lb.com"}, + name: "emissary", + namespace: "emissary-ingress", + }, + ambassadorHostItems: []fakeAmbassadorHost{ + { + name: "fake1", + namespace: "testing1", + hostname: "fake1.org", + annotations: map[string]string{ + "external-dns.ambassador-service": "emissary-ingress/emissary", + "kubernetes.io/ingress.class": "internal-ingress", + }, + }, + }, + expected: []*endpoint.Endpoint{}, + expectError: true, + }, + { + title: "valid matching annotation filter label", + annotationFilter: "kubernetes.io/ingress.class=external-ingress", + labelSelector: labels.Everything(), + loadBalancer: fakeAmbassadorLoadBalancerService{ + ips: []string{"8.8.8.8"}, + hostnames: []string{"lb.com"}, + name: "emissary", + namespace: "emissary-ingress", + }, + ambassadorHostItems: []fakeAmbassadorHost{ + { + name: "fake1", + namespace: "testing1", + hostname: "fake1.org", + annotations: map[string]string{ + "external-dns.ambassador-service": "emissary-ingress/emissary", + "kubernetes.io/ingress.class": "external-ingress", + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "fake1.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, + }, + { + DNSName: "fake1.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, + }, + }, + }, + { + title: "valid non-matching annotation filter label", + annotationFilter: "kubernetes.io/ingress.class=external-ingress", + labelSelector: labels.Everything(), + loadBalancer: fakeAmbassadorLoadBalancerService{ + ips: []string{"8.8.8.8"}, + hostnames: []string{"lb.com"}, + name: "emissary", + namespace: "emissary-ingress", + }, + ambassadorHostItems: []fakeAmbassadorHost{ + { + name: "fake1", + namespace: "testing1", + hostname: "fake1.org", + annotations: map[string]string{ + "external-dns.ambassador-service": "emissary-ingress/emissary", + "kubernetes.io/ingress.class": "internal-ingress", + }, + }, + }, + expected: []*endpoint.Endpoint{}, + }, + { + title: "valid non-matching label filter expression", + labelSelector: labels.SelectorFromSet(labels.Set{"kubernetes.io/ingress.class": "external-ingress"}), + loadBalancer: fakeAmbassadorLoadBalancerService{ + ips: []string{"8.8.8.8"}, + hostnames: []string{"lb.com"}, + name: "emissary", + namespace: "emissary-ingress", + }, + ambassadorHostItems: []fakeAmbassadorHost{ + { + name: "fake1", + namespace: "testing1", + hostname: "fake1.org", + annotations: map[string]string{ + "external-dns.ambassador-service": "emissary-ingress/emissary", + }, + labels: map[string]string{ + "kubernetes.io/ingress.class": "internal-ingress", + }, + }, + }, + expected: []*endpoint.Endpoint{}, + }, + { + title: "valid matching label filter expression for single host", + labelSelector: labels.SelectorFromSet(labels.Set{"kubernetes.io/ingress.class": "external-ingress"}), + loadBalancer: fakeAmbassadorLoadBalancerService{ + ips: []string{"8.8.8.8"}, + hostnames: []string{"lb.com"}, + name: "emissary", + namespace: "emissary-ingress", + }, + ambassadorHostItems: []fakeAmbassadorHost{ + { + name: "fake1", + namespace: "testing1", + hostname: "fake1.org", + annotations: map[string]string{ + "external-dns.ambassador-service": "emissary-ingress/emissary", + }, + labels: map[string]string{ + "kubernetes.io/ingress.class": "external-ingress", + }, + }, + { + name: "fake2", + namespace: "testing2", + hostname: "fake2.org", + annotations: map[string]string{ + "external-dns.ambassador-service": "emissary-ingress/emissary", + "kubernetes.io/ingress.class": "internal-ingress", + }, + labels: map[string]string{ + "kubernetes.io/ingress.class": "internal-ingress", + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "fake1.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, + }, + { + DNSName: "fake1.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, + }, + }, + }, + { + title: "valid matching label filter expression and matching annotation filter", + annotationFilter: "kubernetes.io/ingress.class in (external-ingress)", + labelSelector: labels.SelectorFromSet(labels.Set{"kubernetes.io/ingress.class": "external-ingress"}), + loadBalancer: fakeAmbassadorLoadBalancerService{ + ips: []string{"8.8.8.8"}, + hostnames: []string{"lb.com"}, + name: "emissary", + namespace: "emissary-ingress", + }, + ambassadorHostItems: []fakeAmbassadorHost{ + { + name: "fake1", + namespace: "testing1", + hostname: "fake1.org", + annotations: map[string]string{ + "external-dns.ambassador-service": "emissary-ingress/emissary", + "kubernetes.io/ingress.class": "external-ingress", + }, + labels: map[string]string{ + "kubernetes.io/ingress.class": "external-ingress", + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "fake1.org", + RecordType: endpoint.RecordTypeA, + Targets: endpoint.Targets{"8.8.8.8"}, + }, + { + DNSName: "fake1.org", + RecordType: endpoint.RecordTypeCNAME, + Targets: endpoint.Targets{"lb.com"}, + }, + }, + }, + { + title: "valid non matching label filter expression and valid matching annotation filter", + annotationFilter: "kubernetes.io/ingress.class in (external-ingress)", + labelSelector: labels.SelectorFromSet(labels.Set{"kubernetes.io/ingress.class": "external-ingress"}), + loadBalancer: fakeAmbassadorLoadBalancerService{ + ips: []string{"8.8.8.8"}, + hostnames: []string{"lb.com"}, + name: "emissary", + namespace: "emissary-ingress", + }, + ambassadorHostItems: []fakeAmbassadorHost{ + { + name: "fake1", + namespace: "testing1", + hostname: "fake1.org", + annotations: map[string]string{ + "external-dns.ambassador-service": "emissary-ingress/emissary", + "kubernetes.io/ingress.class": "external-ingress", + }, + labels: map[string]string{ + "kubernetes.io/ingress.class": "internal-ingress", + }, + }, + }, + expected: []*endpoint.Endpoint{}, + }, + { + title: "valid matching label filter expression and non matching annotation filter", + annotationFilter: "kubernetes.io/ingress.class in (external-ingress)", + labelSelector: labels.SelectorFromSet(labels.Set{"kubernetes.io/ingress.class": "external-ingress"}), + loadBalancer: fakeAmbassadorLoadBalancerService{ + ips: []string{"8.8.8.8"}, + hostnames: []string{"lb.com"}, + name: "emissary", + namespace: "emissary-ingress", + }, + ambassadorHostItems: []fakeAmbassadorHost{ + { + name: "fake1", + namespace: "testing1", + hostname: "fake1.org", + annotations: map[string]string{ + "external-dns.ambassador-service": "emissary-ingress/emissary", + "kubernetes.io/ingress.class": "internal-ingress", + }, + labels: map[string]string{ + "kubernetes.io/ingress.class": "external-ingress", + }, + }, + }, + expected: []*endpoint.Endpoint{}, + }, + } { + ti := ti + t.Run(ti.title, func(t *testing.T) { + // Create a slice of Ambassador Hosts + ambassadorHosts := []*ambassador.Host{} + + // Convert our test data into Ambassador Hosts + for _, host := range ti.ambassadorHostItems { + ambassadorHosts = append(ambassadorHosts, host.Host()) + } + + fakeKubernetesClient := fake.NewSimpleClientset() + service := ti.loadBalancer.Service() + _, err := fakeKubernetesClient.CoreV1().Services(service.Namespace).Create(context.Background(), service, metav1.CreateOptions{}) + require.NoError(t, err) + + fakeDynamicClient, scheme := newAmbassadorDynamicKubernetesClient() + + for _, host := range ambassadorHosts { + converted, err := convertAmbassadorHostToUnstructured(host, scheme) + require.NoError(t, err) + _, err = fakeDynamicClient.Resource(ambHostGVR).Namespace(host.Namespace).Create(context.Background(), converted, metav1.CreateOptions{}) + require.NoError(t, err) + } + + ambassadorHostSource, err := NewAmbassadorHostSource( + context.TODO(), + fakeDynamicClient, + fakeKubernetesClient, + ti.targetNamespace, + ti.annotationFilter, + ti.labelSelector, + ) + require.NoError(t, err) + + res, err := ambassadorHostSource.Endpoints(context.Background()) + if ti.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + validateEndpoints(t, res, ti.expected) + }) + } +} + // TestParseAmbLoadBalancerService tests our parsing of Ambassador service info. func TestParseAmbLoadBalancerService(t *testing.T) { vectors := []struct { @@ -140,3 +675,77 @@ func TestParseAmbLoadBalancerService(t *testing.T) { } } } + +func convertAmbassadorHostToUnstructured(hp *ambassador.Host, s *runtime.Scheme) (*unstructured.Unstructured, error) { + unstructuredAmbassadorHost := &unstructured.Unstructured{} + if err := s.Convert(hp, unstructuredAmbassadorHost, context.Background()); err != nil { + return nil, err + } + return unstructuredAmbassadorHost, nil +} + +func newAmbassadorDynamicKubernetesClient() (*fakeDynamic.FakeDynamicClient, *runtime.Scheme) { + s := runtime.NewScheme() + _ = ambassador.AddToScheme(s) + return fakeDynamic.NewSimpleDynamicClient(s), s +} + +type fakeAmbassadorHost struct { + namespace string + name string + annotations map[string]string + hostname string + labels map[string]string +} + +func (ir fakeAmbassadorHost) Host() *ambassador.Host { + spec := ambassador.HostSpec{ + Hostname: ir.hostname, + } + + host := &ambassador.Host{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ir.namespace, + Name: ir.name, + Annotations: ir.annotations, + Labels: ir.labels, + }, + Spec: &spec, + } + + return host +} + +type fakeAmbassadorLoadBalancerService struct { + ips []string + hostnames []string + namespace string + name string +} + +func (ig fakeAmbassadorLoadBalancerService) Service() *v1.Service { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ig.namespace, + Name: ig.name, + }, + Status: v1.ServiceStatus{ + LoadBalancer: v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{}, + }, + }, + } + + for _, ip := range ig.ips { + svc.Status.LoadBalancer.Ingress = append(svc.Status.LoadBalancer.Ingress, v1.LoadBalancerIngress{ + IP: ip, + }) + } + for _, hostname := range ig.hostnames { + svc.Status.LoadBalancer.Ingress = append(svc.Status.LoadBalancer.Ingress, v1.LoadBalancerIngress{ + Hostname: hostname, + }) + } + + return svc +} diff --git a/source/store.go b/source/store.go index cdb993b6b7..f67091d315 100644 --- a/source/store.go +++ b/source/store.go @@ -276,7 +276,7 @@ func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg if err != nil { return nil, err } - return NewAmbassadorHostSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace) + return NewAmbassadorHostSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.LabelFilter) case "contour-httpproxy": dynamicClient, err := p.DynamicKubernetesClient() if err != nil {