diff --git a/docs/tutorials/kong.md b/docs/tutorials/kong.md new file mode 100644 index 000000000..d03ad9681 --- /dev/null +++ b/docs/tutorials/kong.md @@ -0,0 +1,95 @@ +# Configuring ExternalDNS to use the Kong TCPIngress Source +This tutorial describes how to configure ExternalDNS to use the Kong TCPIngress source. +It is meant to supplement the other provider-specific setup tutorials. + +### Manifest (for clusters without RBAC enabled) +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + selector: + matchLabels: + app: external-dns + template: + metadata: + labels: + app: external-dns + spec: + containers: + - name: external-dns + # update this to the desired external-dns version + image: k8s.gcr.io/external-dns/external-dns:v0.9.0 + args: + - --source=kong-tcpingress + - --provider=aws + - --registry=txt + - --txt-owner-id=my-identifier +``` + +### Manifest (for clusters with RBAC enabled) +Could be changed if you have mulitple sources + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: external-dns +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: external-dns +rules: +- apiGroups: [""] + resources: ["services","endpoints","pods"] + verbs: ["get","watch","list"] +- apiGroups: [""] + resources: ["nodes"] + verbs: ["list","watch"] +- apiGroups: ["configuration.konghq.com"] + resources: ["tcpingresses"] + verbs: ["get","watch","list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: external-dns-viewer +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: external-dns +subjects: +- kind: ServiceAccount + name: external-dns + namespace: default +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + selector: + matchLabels: + app: external-dns + template: + metadata: + labels: + app: external-dns + spec: + serviceAccountName: external-dns + containers: + - name: external-dns + # update this to the desired external-dns version + image: k8s.gcr.io/external-dns/external-dns:v0.9.0 + args: + - --source=kong-tcpingress + - --provider=aws + - --registry=txt + - --txt-owner-id=my-identifier +``` diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index b77a9b651..9421b8559 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -352,7 +352,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("skipper-routegroup-groupversion", "The resource version for skipper routegroup").Default(source.DefaultRoutegroupVersion).StringVar(&cfg.SkipperRouteGroupVersion) // Flags related to processing sources - app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "pod", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host") + app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "pod", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host", "kong-tcpingress") app.Flag("namespace", "Limit sources of endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace) app.Flag("annotation-filter", "Filter sources managed by external-dns via annotation using label selector semantics (default: all sources)").Default(defaultConfig.AnnotationFilter).StringVar(&cfg.AnnotationFilter) app.Flag("label-filter", "Filter sources managed by external-dns via label selector when listing all resources; currently only supported by source CRD").Default(defaultConfig.LabelFilter).StringVar(&cfg.LabelFilter) diff --git a/source/kong_tcpingress.go b/source/kong_tcpingress.go new file mode 100644 index 000000000..1fe0b1111 --- /dev/null +++ b/source/kong_tcpingress.go @@ -0,0 +1,455 @@ +/* +Copyright 2021 The Kubernetes 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 source + +import ( + "context" + "fmt" + "sort" + "time" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + corev1 "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" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/dynamic/dynamicinformer" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/cache" + + "sigs.k8s.io/external-dns/endpoint" +) + +var kongGroupdVersionResource = schema.GroupVersionResource{ + Group: "configuration.konghq.com", + Version: "v1beta1", + Resource: "tcpingresses", +} + +// kongTCPIngressSource is an implementation of Source for Kong TCPIngress objects. +type kongTCPIngressSource struct { + annotationFilter string + dynamicKubeClient dynamic.Interface + kongTCPIngressInformer informers.GenericInformer + kubeClient kubernetes.Interface + namespace string + unstructuredConverter *unstructuredConverter +} + +// NewKongTCPIngressSource creates a new kongTCPIngressSource with the given config. +func NewKongTCPIngressSource(dynamicKubeClient dynamic.Interface, kubeClient kubernetes.Interface, namespace string, annotationFilter string) (Source, error) { + var err error + + // Use shared informer to listen for add/update/delete of Host in the specified namespace. + // Set resync period to 0, to prevent processing when nothing has changed. + informerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicKubeClient, 0, namespace, nil) + kongTCPIngressInformer := informerFactory.ForResource(kongGroupdVersionResource) + + // Add default resource event handlers to properly initialize informer. + kongTCPIngressInformer.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + }, + }, + ) + + // TODO informer is not explicitly stopped since controller is not passing in its channel. + informerFactory.Start(wait.NeverStop) + + // wait for the local cache to be populated. + err = poll(time.Second, 60*time.Second, func() (bool, error) { + return kongTCPIngressInformer.Informer().HasSynced(), nil + }) + if err != nil { + return nil, errors.Wrapf(err, "failed to sync cache") + } + + uc, err := newKongUnstructuredConverter() + if err != nil { + return nil, errors.Wrapf(err, "failed to setup Unstructured Converter") + } + + return &kongTCPIngressSource{ + annotationFilter: annotationFilter, + dynamicKubeClient: dynamicKubeClient, + kongTCPIngressInformer: kongTCPIngressInformer, + kubeClient: kubeClient, + namespace: namespace, + unstructuredConverter: uc, + }, nil +} + +// Endpoints returns endpoint objects for each host-target combination that should be processed. +// Retrieves all TCPIngresses in the source's namespace(s). +func (sc *kongTCPIngressSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { + tis, err := sc.kongTCPIngressInformer.Lister().ByNamespace(sc.namespace).List(labels.Everything()) + if err != nil { + return nil, err + } + + var tcpIngresses []*TCPIngress + for _, tcpIngressObj := range tis { + unstructuredHost, ok := tcpIngressObj.(*unstructured.Unstructured) + if !ok { + return nil, errors.New("could not convert") + } + + tcpIngress := &TCPIngress{} + err := sc.unstructuredConverter.scheme.Convert(unstructuredHost, tcpIngress, nil) + if err != nil { + return nil, err + } + tcpIngresses = append(tcpIngresses, tcpIngress) + } + + tcpIngresses, err = sc.filterByAnnotations(tcpIngresses) + if err != nil { + return nil, errors.Wrap(err, "failed to filter TCPIngresses") + } + + var endpoints []*endpoint.Endpoint + for _, tcpIngress := range tcpIngresses { + var targets endpoint.Targets + for _, lb := range tcpIngress.Status.LoadBalancer.Ingress { + if lb.IP != "" { + targets = append(targets, lb.IP) + } + if lb.Hostname != "" { + targets = append(targets, lb.Hostname) + } + } + + fullname := fmt.Sprintf("%s/%s", tcpIngress.Namespace, tcpIngress.Name) + + ingressEndpoints, err := sc.endpointsFromTCPIngress(tcpIngress, targets) + if err != nil { + return nil, err + } + if len(ingressEndpoints) == 0 { + log.Debugf("No endpoints could be generated from Host %s", fullname) + continue + } + + log.Debugf("Endpoints generated from TCPIngress: %s: %v", fullname, ingressEndpoints) + sc.setResourceLabel(tcpIngress, ingressEndpoints) + sc.setDualstackLabel(tcpIngress, ingressEndpoints) + endpoints = append(endpoints, ingressEndpoints...) + } + + for _, ep := range endpoints { + sort.Sort(ep.Targets) + } + + return endpoints, nil +} + +// filterByAnnotations filters a list of TCPIngresses by a given annotation selector. +func (sc *kongTCPIngressSource) filterByAnnotations(tcpIngresses []*TCPIngress) ([]*TCPIngress, error) { + 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 + if selector.Empty() { + return tcpIngresses, nil + } + + filteredList := []*TCPIngress{} + + for _, tcpIngress := range tcpIngresses { + // convert the TCPIngress's annotations to an equivalent label selector + annotations := labels.Set(tcpIngress.Annotations) + + // include TCPIngress if its annotations match the selector + if selector.Matches(annotations) { + filteredList = append(filteredList, tcpIngress) + } + } + + return filteredList, nil +} + +func (sc *kongTCPIngressSource) setResourceLabel(tcpIngress *TCPIngress, endpoints []*endpoint.Endpoint) { + for _, ep := range endpoints { + ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("tcpingress/%s/%s", tcpIngress.Namespace, tcpIngress.Name) + } +} + +func (sc *kongTCPIngressSource) setDualstackLabel(tcpIngress *TCPIngress, endpoints []*endpoint.Endpoint) { + val, ok := tcpIngress.Annotations[ALBDualstackAnnotationKey] + if ok && val == ALBDualstackAnnotationValue { + log.Debugf("Adding dualstack label to TCPIngress %s/%s.", tcpIngress.Namespace, tcpIngress.Name) + for _, ep := range endpoints { + ep.Labels[endpoint.DualstackLabelKey] = "true" + } + } +} + +// endpointsFromTCPIngress extracts the endpoints from a TCPIngress object +func (sc *kongTCPIngressSource) endpointsFromTCPIngress(tcpIngress *TCPIngress, targets endpoint.Targets) ([]*endpoint.Endpoint, error) { + var endpoints []*endpoint.Endpoint + + providerSpecific, setIdentifier := getProviderSpecificAnnotations(tcpIngress.Annotations) + + ttl, err := getTTLFromAnnotations(tcpIngress.Annotations) + if err != nil { + return nil, err + } + + hostnameList := getHostnamesFromAnnotations(tcpIngress.Annotations) + for _, hostname := range hostnameList { + endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...) + } + + if tcpIngress.Spec.Rules != nil { + for _, rule := range tcpIngress.Spec.Rules { + if rule.Host != "" { + endpoints = append(endpoints, endpointsForHostname(rule.Host, targets, ttl, providerSpecific, setIdentifier)...) + } + } + } + + return endpoints, nil +} + +func (sc *kongTCPIngressSource) AddEventHandler(ctx context.Context, handler func()) { + log.Debug("Adding event handler for TCPIngress") + + // Right now there is no way to remove event handler from informer, see: + // https://github.com/kubernetes/kubernetes/issues/79610 + sc.kongTCPIngressInformer.Informer().AddEventHandler( + cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + handler() + }, + UpdateFunc: func(old interface{}, new interface{}) { + handler() + }, + DeleteFunc: func(obj interface{}) { + handler() + }, + }, + ) +} + +// newUnstructuredConverter returns a new unstructuredConverter initialized +func newKongUnstructuredConverter() (*unstructuredConverter, error) { + uc := &unstructuredConverter{ + scheme: runtime.NewScheme(), + } + + // Add the core types we need + uc.scheme.AddKnownTypes(kongGroupdVersionResource.GroupVersion(), &TCPIngress{}, &TCPIngressList{}) + if err := scheme.AddToScheme(uc.scheme); err != nil { + return nil, err + } + + return uc, nil +} + +//Kong types based on https://github.com/Kong/kubernetes-ingress-controller/blob/v1.2.0/pkg/apis/configuration/v1beta1/types.go to facilitate testing +//When trying to import them from the Kong repo as a dependency it required upgrading the k8s.io/client-go and k8s.io/apimachinery which seemed +//cause several changes in how the mock clients were working that resulted in a bunch of failures in other tests +//If that is dealt with at some point the below can be removed and replaced with an actual import + +type TCPIngress struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec tcpIngressSpec `json:"spec,omitempty"` + Status tcpIngressStatus `json:"status,omitempty"` +} + +type TCPIngressList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []TCPIngress `json:"items"` +} + +type tcpIngressSpec struct { + Rules []tcpIngressRule `json:"rules,omitempty"` + TLS []tcpIngressTLS `json:"tls,omitempty"` +} + +type tcpIngressTLS struct { + Hosts []string `json:"hosts,omitempty"` + SecretName string `json:"secretName,omitempty"` +} + +type tcpIngressStatus struct { + LoadBalancer corev1.LoadBalancerStatus `json:"loadBalancer,omitempty"` +} + +type tcpIngressRule struct { + Host string `json:"host,omitempty"` + Port int `json:"port,omitempty"` + Backend tcpIngressBackend `json:"backend"` +} + +type tcpIngressBackend struct { + ServiceName string `json:"serviceName"` + ServicePort int `json:"servicePort"` +} + +func (in *tcpIngressBackend) DeepCopyInto(out *tcpIngressBackend) { + *out = *in +} + +func (in *tcpIngressBackend) DeepCopy() *tcpIngressBackend { + if in == nil { + return nil + } + out := new(tcpIngressBackend) + in.DeepCopyInto(out) + return out +} + +func (in *tcpIngressRule) DeepCopyInto(out *tcpIngressRule) { + *out = *in + out.Backend = in.Backend +} + +func (in *tcpIngressRule) DeepCopy() *tcpIngressRule { + if in == nil { + return nil + } + out := new(tcpIngressRule) + in.DeepCopyInto(out) + return out +} + +func (in *tcpIngressSpec) DeepCopyInto(out *tcpIngressSpec) { + *out = *in + if in.Rules != nil { + in, out := &in.Rules, &out.Rules + *out = make([]tcpIngressRule, len(*in)) + copy(*out, *in) + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = make([]tcpIngressTLS, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +func (in *tcpIngressSpec) DeepCopy() *tcpIngressSpec { + if in == nil { + return nil + } + out := new(tcpIngressSpec) + in.DeepCopyInto(out) + return out +} + +func (in *tcpIngressStatus) DeepCopyInto(out *tcpIngressStatus) { + *out = *in + in.LoadBalancer.DeepCopyInto(&out.LoadBalancer) +} + +func (in *tcpIngressStatus) DeepCopy() *tcpIngressStatus { + if in == nil { + return nil + } + out := new(tcpIngressStatus) + in.DeepCopyInto(out) + return out +} + +func (in *tcpIngressTLS) DeepCopyInto(out *tcpIngressTLS) { + *out = *in + if in.Hosts != nil { + in, out := &in.Hosts, &out.Hosts + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +func (in *tcpIngressTLS) DeepCopy() *tcpIngressTLS { + if in == nil { + return nil + } + out := new(tcpIngressTLS) + in.DeepCopyInto(out) + return out +} + +func (in *TCPIngress) DeepCopyInto(out *TCPIngress) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +func (in *TCPIngress) DeepCopy() *TCPIngress { + if in == nil { + return nil + } + out := new(TCPIngress) + in.DeepCopyInto(out) + return out +} + +func (in *TCPIngress) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +func (in *TCPIngressList) DeepCopyInto(out *TCPIngressList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TCPIngress, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +func (in *TCPIngressList) DeepCopy() *TCPIngressList { + if in == nil { + return nil + } + out := new(TCPIngressList) + in.DeepCopyInto(out) + return out +} + +func (in *TCPIngressList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/source/kong_tcpingress_test.go b/source/kong_tcpingress_test.go new file mode 100644 index 000000000..b076ecd7b --- /dev/null +++ b/source/kong_tcpingress_test.go @@ -0,0 +1,253 @@ +/* +Copyright 2021 The Kubernetes 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 source + +import ( + "context" + "encoding/json" + "github.com/stretchr/testify/assert" + corev1 "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/runtime" + fakeDynamic "k8s.io/client-go/dynamic/fake" + fakeKube "k8s.io/client-go/kubernetes/fake" + "sigs.k8s.io/external-dns/endpoint" + "testing" +) + +// This is a compile-time validation that glooSource is a Source. +var _ Source = &kongTCPIngressSource{} + +const defaultKongNamespace = "kong" + +func TestKongTCPIngressEndpoints(t *testing.T) { + for _, ti := range []struct { + title string + tcpProxy TCPIngress + expected []*endpoint.Endpoint + }{ + { + title: "TCPIngress with hostname annotation", + tcpProxy: TCPIngress{ + TypeMeta: metav1.TypeMeta{ + APIVersion: kongGroupdVersionResource.GroupVersion().String(), + Kind: "TCPIngress", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "tcp-ingress-annotation", + Namespace: defaultKongNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "a.example.com", + "kubernetes.io/ingress.class": "kong", + }, + }, + Spec: tcpIngressSpec{ + Rules: []tcpIngressRule{ + { + Port: 30000, + }, + { + Port: 30001, + }, + }, + }, + Status: tcpIngressStatus{ + LoadBalancer: corev1.LoadBalancerStatus{ + Ingress: []corev1.LoadBalancerIngress{ + { + Hostname: "a691234567a314e71861a4303f06a3bd-1291189659.us-east-1.elb.amazonaws.com", + }, + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "a.example.com", + Targets: []string{"a691234567a314e71861a4303f06a3bd-1291189659.us-east-1.elb.amazonaws.com"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "tcpingress/kong/tcp-ingress-annotation", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "TCPIngress using SNI", + tcpProxy: TCPIngress{ + TypeMeta: metav1.TypeMeta{ + APIVersion: kongGroupdVersionResource.GroupVersion().String(), + Kind: "TCPIngress", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "tcp-ingress-sni", + Namespace: defaultKongNamespace, + Annotations: map[string]string{ + "kubernetes.io/ingress.class": "kong", + }, + }, + Spec: tcpIngressSpec{ + Rules: []tcpIngressRule{ + { + Port: 30002, + Host: "b.example.com", + }, + { + Port: 30003, + Host: "c.example.com", + }, + }, + }, + Status: tcpIngressStatus{ + LoadBalancer: corev1.LoadBalancerStatus{ + Ingress: []corev1.LoadBalancerIngress{ + { + Hostname: "a123456769a314e71861a4303f06a3bd-1291189659.us-east-1.elb.amazonaws.com", + }, + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "b.example.com", + Targets: []string{"a123456769a314e71861a4303f06a3bd-1291189659.us-east-1.elb.amazonaws.com"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "tcpingress/kong/tcp-ingress-sni", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "c.example.com", + Targets: []string{"a123456769a314e71861a4303f06a3bd-1291189659.us-east-1.elb.amazonaws.com"}, + RecordType: endpoint.RecordTypeCNAME, + Labels: endpoint.Labels{ + "resource": "tcpingress/kong/tcp-ingress-sni", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + title: "TCPIngress with hostname annotation and using SNI", + tcpProxy: TCPIngress{ + TypeMeta: metav1.TypeMeta{ + APIVersion: kongGroupdVersionResource.GroupVersion().String(), + Kind: "TCPIngress", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "tcp-ingress-both", + Namespace: defaultKongNamespace, + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "d.example.com", + "kubernetes.io/ingress.class": "kong", + }, + }, + Spec: tcpIngressSpec{ + Rules: []tcpIngressRule{ + { + Port: 30004, + Host: "e.example.com", + }, + { + Port: 30005, + Host: "f.example.com", + }, + }, + }, + Status: tcpIngressStatus{ + LoadBalancer: corev1.LoadBalancerStatus{ + Ingress: []corev1.LoadBalancerIngress{ + { + Hostname: "a12e71861a4303f063456769a314a3bd-1291189659.us-east-1.elb.amazonaws.com", + }, + }, + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "d.example.com", + Targets: []string{"a12e71861a4303f063456769a314a3bd-1291189659.us-east-1.elb.amazonaws.com"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "tcpingress/kong/tcp-ingress-both", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "e.example.com", + Targets: []string{"a12e71861a4303f063456769a314a3bd-1291189659.us-east-1.elb.amazonaws.com"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "tcpingress/kong/tcp-ingress-both", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + { + DNSName: "f.example.com", + Targets: []string{"a12e71861a4303f063456769a314a3bd-1291189659.us-east-1.elb.amazonaws.com"}, + RecordType: endpoint.RecordTypeCNAME, + RecordTTL: 0, + Labels: endpoint.Labels{ + "resource": "tcpingress/kong/tcp-ingress-both", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + } { + t.Run(ti.title, func(t *testing.T) { + fakeKubernetesClient := fakeKube.NewSimpleClientset() + scheme := runtime.NewScheme() + scheme.AddKnownTypes(kongGroupdVersionResource.GroupVersion(), &TCPIngress{}, &TCPIngressList{}) + fakeDynamicClient := fakeDynamic.NewSimpleDynamicClient(scheme) + + tcpi := unstructured.Unstructured{} + + tcpIngressAsJSON, err := json.Marshal(ti.tcpProxy) + assert.NoError(t, err) + + assert.NoError(t, tcpi.UnmarshalJSON(tcpIngressAsJSON)) + + // Create proxy resources + _, err = fakeDynamicClient.Resource(kongGroupdVersionResource).Namespace(defaultKongNamespace).Create(context.Background(), &tcpi, metav1.CreateOptions{}) + assert.NoError(t, err) + + source, err := NewKongTCPIngressSource(fakeDynamicClient, fakeKubernetesClient, defaultKongNamespace, "kubernetes.io/ingress.class=kong") + assert.NoError(t, err) + assert.NotNil(t, source) + + count := &unstructured.UnstructuredList{} + for len(count.Items) < 1 { + count, _ = fakeDynamicClient.Resource(kongGroupdVersionResource).Namespace(defaultKongNamespace).List(context.Background(), metav1.ListOptions{}) + } + + endpoints, err := source.Endpoints(context.Background()) + assert.NoError(t, err) + assert.Len(t, endpoints, len(ti.expected)) + assert.Equal(t, endpoints, ti.expected) + }) + } +} diff --git a/source/store.go b/source/store.go index 4f9169f4b..355ec4ffa 100644 --- a/source/store.go +++ b/source/store.go @@ -288,6 +288,16 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err token = restConfig.BearerToken } return NewRouteGroupSource(cfg.RequestTimeout, token, tokenPath, apiServerURL, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.SkipperRouteGroupVersion, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) + case "kong-tcpingress": + kubernetesClient, err := p.KubeClient() + if err != nil { + return nil, err + } + dynamicClient, err := p.DynamicKubernetesClient() + if err != nil { + return nil, err + } + return NewKongTCPIngressSource(dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter) } return nil, ErrSourceNotFound } diff --git a/source/store_test.go b/source/store_test.go index e79c6772c..4fa787bbf 100644 --- a/source/store_test.go +++ b/source/store_test.go @@ -96,9 +96,9 @@ func (suite *ByNamesTestSuite) TestAllInitialized() { mockClientGenerator.On("IstioClient").Return(NewFakeConfigStore(), nil) mockClientGenerator.On("DynamicKubernetesClient").Return(fakeDynamic, nil) - sources, err := ByNames(mockClientGenerator, []string{"service", "ingress", "istio-gateway", "contour-ingressroute", "contour-httpproxy", "fake"}, minimalConfig) + sources, err := ByNames(mockClientGenerator, []string{"service", "ingress", "istio-gateway", "contour-ingressroute", "contour-httpproxy", "kong-tcpingress", "fake"}, minimalConfig) suite.NoError(err, "should not generate errors") - suite.Len(sources, 6, "should generate all six sources") + suite.Len(sources, 7, "should generate all six sources") } func (suite *ByNamesTestSuite) TestOnlyFake() { @@ -135,6 +135,9 @@ func (suite *ByNamesTestSuite) TestKubeClientFails() { _, err = ByNames(mockClientGenerator, []string{"contour-ingressroute"}, minimalConfig) suite.Error(err, "should return an error if kubernetes client cannot be created") + + _, err = ByNames(mockClientGenerator, []string{"kong-tcpingress"}, minimalConfig) + suite.Error(err, "should return an error if kubernetes client cannot be created") } func (suite *ByNamesTestSuite) TestIstioClientFails() {