diff --git a/docs/advanced/fqdn-templating.md b/docs/advanced/fqdn-templating.md index 8189c8c30..82ca6c249 100644 --- a/docs/advanced/fqdn-templating.md +++ b/docs/advanced/fqdn-templating.md @@ -33,33 +33,33 @@ The template uses the following data from the source object (e.g., a `Service` o -| Source | Description | FQDN Supported | -|:-----------------------|:----------------------------------------------------------------|:--------------:| -| `ambassador-host` | Queries Ambassador Host resources for endpoints. | ❌ | -| `cloudfoundry` | Queries Cloud Foundry resources for endpoints. | ❌ | -| `connector` | Queries a custom connector source for endpoints. | ❌ | -| `contour-httpproxy` | Queries Contour HTTPProxy resources for endpoints. | ✅ | -| `crd` | Queries Custom Resource Definitions (CRDs) for endpoints. | ❌ | -| `empty` | Uses an empty source, typically for testing or no-op scenarios. | ❌ | -| `f5-transportserver` | Queries F5 TransportServer resources for endpoints. | ❌ | -| `f5-virtualserver` | Queries F5 VirtualServer resources for endpoints. | ❌ | -| `fake` | Uses a fake source for testing purposes. | ❌ | -| `gateway-grpcroute` | Queries GRPCRoute resources from the Gateway API. | ✅ | -| `gateway-httproute` | Queries HTTPRoute resources from the Gateway API. | ✅ | -| `gateway-tcproute` | Queries TCPRoute resources from the Gateway API. | ✅ | -| `gateway-tlsroute` | Queries TLSRoute resources from the Gateway API. | ❌ | -| `gateway-udproute` | Queries UDPRoute resources from the Gateway API. | ❌ | -| `gloo-proxy` | Queries Gloo Proxy resources for endpoints. | ❌ | -| `ingress` | Queries Kubernetes Ingress resources for endpoints. | ✅ | -| `istio-gateway` | Queries Istio Gateway resources for endpoints. | ✅ | -| `istio-virtualservice` | Queries Istio VirtualService resources for endpoints. | ✅ | -| `kong-tcpingress` | Queries Kong TCPIngress resources for endpoints. | ❌ | -| `node` | Queries Kubernetes Node resources for endpoints. | ✅ | -| `openshift-route` | Queries OpenShift Route resources for endpoints. | ✅ | -| `pod` | Queries Kubernetes Pod resources for endpoints. | ❌ | -| `service` | Queries Kubernetes Service resources for endpoints. | ✅ | -| `skipper-routegroup` | Queries Skipper RouteGroup resources for endpoints. | ✅ | -| `traefik-proxy` | Queries Traefik Proxy resources for endpoints. | ❌ | +| Source | Description | FQDN Supported | FQDN Combine | +|:-----------------------|:----------------------------------------------------------------|:--------------:|:------------:| +| `ambassador-host` | Queries Ambassador Host resources for endpoints. | ❌ | ❌ | +| `cloudfoundry` | Queries Cloud Foundry resources for endpoints. | ❌ | ❌ | +| `connector` | Queries a custom connector source for endpoints. | ❌ | ❌ | +| `contour-httpproxy` | Queries Contour HTTPProxy resources for endpoints. | ✅ | ✅ | +| `crd` | Queries Custom Resource Definitions (CRDs) for endpoints. | ❌ | ❌ | +| `empty` | Uses an empty source, typically for testing or no-op scenarios. | ❌ | ❌ | +| `f5-transportserver` | Queries F5 TransportServer resources for endpoints. | ❌ | ❌ | +| `f5-virtualserver` | Queries F5 VirtualServer resources for endpoints. | ❌ | ❌ | +| `fake` | Uses a fake source for testing purposes. | ❌ | ❌ | +| `gateway-grpcroute` | Queries GRPCRoute resources from the Gateway API. | ✅ | ❌ | +| `gateway-httproute` | Queries HTTPRoute resources from the Gateway API. | ✅ | ❌ | +| `gateway-tcproute` | Queries TCPRoute resources from the Gateway API. | ✅ | ❌ | +| `gateway-tlsroute` | Queries TLSRoute resources from the Gateway API. | ❌ | ❌ | +| `gateway-udproute` | Queries UDPRoute resources from the Gateway API. | ❌ | ❌ | +| `gloo-proxy` | Queries Gloo Proxy resources for endpoints. | ❌ | ❌ | +| `ingress` | Queries Kubernetes Ingress resources for endpoints. | ✅ | ✅ | +| `istio-gateway` | Queries Istio Gateway resources for endpoints. | ✅ | ✅ | +| `istio-virtualservice` | Queries Istio VirtualService resources for endpoints. | ✅ | ✅ | +| `kong-tcpingress` | Queries Kong TCPIngress resources for endpoints. | ❌ | ❌ | +| `node` | Queries Kubernetes Node resources for endpoints. | ✅ | ❌ | +| `openshift-route` | Queries OpenShift Route resources for endpoints. | ✅ | ✅ | +| `pod` | Queries Kubernetes Pod resources for endpoints. | ✅ | ✅ | +| `service` | Queries Kubernetes Service resources for endpoints. | ✅ | ✅ | +| `skipper-routegroup` | Queries Skipper RouteGroup resources for endpoints. | ✅ | ✅ | +| `traefik-proxy` | Queries Traefik IngressRoute resources for endpoints. | ❌ | ❌ | ## Custom Functions diff --git a/source/pod.go b/source/pod.go index 0e1f53ab2..4ac0c0d5d 100644 --- a/source/pod.go +++ b/source/pod.go @@ -18,6 +18,9 @@ package source import ( "context" + "fmt" + "maps" + "text/template" log "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" @@ -27,14 +30,19 @@ import ( "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" + "sigs.k8s.io/external-dns/source/fqdn" + "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" "sigs.k8s.io/external-dns/source/informers" ) type podSource struct { - client kubernetes.Interface - namespace string + client kubernetes.Interface + namespace string + fqdnTemplate *template.Template + combineFQDNAnnotation bool + podInformer coreinformers.PodInformer nodeInformer coreinformers.NodeInformer compatibility string @@ -43,18 +51,27 @@ type podSource struct { } // NewPodSource creates a new podSource with the given config. -func NewPodSource(ctx context.Context, kubeClient kubernetes.Interface, namespace string, compatibility string, ignoreNonHostNetworkPods bool, podSourceDomain string) (Source, error) { +func NewPodSource( + ctx context.Context, + kubeClient kubernetes.Interface, + namespace string, + compatibility string, + ignoreNonHostNetworkPods bool, + podSourceDomain string, + fqdnTemplate string, + combineFqdnAnnotation bool, +) (Source, error) { informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(kubeClient, 0, kubeinformers.WithNamespace(namespace)) podInformer := informerFactory.Core().V1().Pods() nodeInformer := informerFactory.Core().V1().Nodes() - podInformer.Informer().AddEventHandler( + _, _ = podInformer.Informer().AddEventHandler( cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { }, }, ) - nodeInformer.Informer().AddEventHandler( + _, _ = nodeInformer.Informer().AddEventHandler( cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { }, @@ -68,6 +85,11 @@ func NewPodSource(ctx context.Context, kubeClient kubernetes.Interface, namespac return nil, err } + tmpl, err := fqdn.ParseTemplate(fqdnTemplate) + if err != nil { + return nil, err + } + return &podSource{ client: kubeClient, podInformer: podInformer, @@ -76,13 +98,15 @@ func NewPodSource(ctx context.Context, kubeClient kubernetes.Interface, namespac compatibility: compatibility, ignoreNonHostNetworkPods: ignoreNonHostNetworkPods, podSourceDomain: podSourceDomain, + fqdnTemplate: tmpl, + combineFQDNAnnotation: combineFqdnAnnotation, }, nil } -func (*podSource) AddEventHandler(ctx context.Context, handler func()) { +func (*podSource) AddEventHandler(_ context.Context, _ func()) { } -func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { +func (ps *podSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, error) { pods, err := ps.podInformer.Lister().Pods(ps.namespace).List(labels.Everything()) if err != nil { return nil, err @@ -90,8 +114,19 @@ func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error endpointMap := make(map[endpoint.EndpointKey][]string) for _, pod := range pods { - ps.addPodEndpointsToEndpointMap(endpointMap, pod) + if ps.fqdnTemplate == nil || ps.combineFQDNAnnotation { + ps.addPodEndpointsToEndpointMap(endpointMap, pod) + } + + if ps.fqdnTemplate != nil { + fqdnHosts, err := ps.hostsFromTemplate(pod) + if err != nil { + return nil, err + } + maps.Copy(endpointMap, fqdnHosts) + } } + var endpoints []*endpoint.Endpoint for key, targets := range endpointMap { endpoints = append(endpoints, endpoint.NewEndpoint(key.DNSName, key.RecordType, targets...)) @@ -180,6 +215,30 @@ func (ps *podSource) addPodNodeEndpointsToEndpointMap(endpointMap map[endpoint.E } } +func (ps *podSource) hostsFromTemplate(pod *corev1.Pod) (map[endpoint.EndpointKey][]string, error) { + hosts, err := fqdn.ExecTemplate(ps.fqdnTemplate, pod) + if err != nil { + return nil, fmt.Errorf("skipping generating endpoints from template for pod %s: %w", pod.Name, err) + } + + result := make(map[endpoint.EndpointKey][]string) + for _, target := range hosts { + for _, address := range pod.Status.PodIPs { + if address.IP == "" { + log.Debugf("skipping pod %q. PodIP is empty with phase %q", pod.Name, pod.Status.Phase) + continue + } + key := endpoint.EndpointKey{ + DNSName: target, + RecordType: suitableType(address.IP), + } + result[key] = append(result[key], address.IP) + } + } + + return result, nil +} + func addTargetsToEndpointMap(endpointMap map[endpoint.EndpointKey][]string, targets []string, domainList ...string) { for _, domain := range domainList { for _, target := range targets { diff --git a/source/pod_fqdn_test.go b/source/pod_fqdn_test.go new file mode 100644 index 000000000..28f656220 --- /dev/null +++ b/source/pod_fqdn_test.go @@ -0,0 +1,477 @@ +/* +Copyright 2025 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 ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + "sigs.k8s.io/external-dns/endpoint" +) + +func TestNewPodSourceWithFqdn(t *testing.T) { + for _, tt := range []struct { + title string + annotationFilter string + fqdnTemplate string + expectError bool + }{ + { + title: "invalid template", + expectError: true, + fqdnTemplate: "{{.Name", + }, + { + title: "valid empty template", + expectError: false, + }, + { + title: "valid template", + expectError: false, + fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com", + }, + } { + t.Run(tt.title, func(t *testing.T) { + _, err := NewPodSource( + t.Context(), + fake.NewClientset(), + "", + "", + false, + "", + tt.fqdnTemplate, + false) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestPodSourceFqdnTemplatingExamples(t *testing.T) { + for _, tt := range []struct { + title string + pods []*v1.Pod + nodes []*v1.Node + fqdnTemplate string + expected []*endpoint.Endpoint + combineFQDN bool + sourceDomain string + }{ + { + title: "templating expansion with multiple domains", + pods: []*v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-pod-1", + Namespace: "default", + }, + Status: v1.PodStatus{ + PodIP: "100.67.94.101", + PodIPs: []v1.PodIP{ + {IP: "100.67.94.101"}, + }, + }, + }, + }, + fqdnTemplate: "{{ .Name }}.domainA.com,{{ .Name }}.domainB.com", + expected: []*endpoint.Endpoint{ + {DNSName: "my-pod-1.domainA.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.67.94.101"}}, + {DNSName: "my-pod-1.domainB.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.67.94.101"}}, + }, + }, + { + title: "templating expansion with multiple domains and fqdn combine and pod source domain", + combineFQDN: true, + sourceDomain: "example.org", + pods: []*v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-pod-1", + Namespace: "default", + }, + Spec: v1.PodSpec{ + NodeName: "node-1.internal", + }, + Status: v1.PodStatus{ + PodIP: "100.67.94.101", + PodIPs: []v1.PodIP{ + {IP: "100.67.94.101"}, + }, + }, + }, + }, + nodes: []*v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node-1.internal", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeExternalIP, Address: "10.1.192.139"}, + }, + }, + }, + }, + fqdnTemplate: "{{ .Name }}.domainA.com,{{ .Name }}.domainB.com", + expected: []*endpoint.Endpoint{ + {DNSName: "my-pod-1.domainA.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.67.94.101"}}, + {DNSName: "my-pod-1.domainB.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.67.94.101"}}, + {DNSName: "my-pod-1.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.67.94.101"}}, + }, + }, + { + title: "templating with domain per namespace", + pods: []*v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-1", + Namespace: "default", + }, + Status: v1.PodStatus{ + PodIP: "100.67.94.101", + PodIPs: []v1.PodIP{ + {IP: "100.67.94.101"}, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-2", + Namespace: "kube-system", + }, + Status: v1.PodStatus{ + PodIP: "100.67.94.102", + PodIPs: []v1.PodIP{ + {IP: "100.67.94.102"}, + }, + }, + }, + }, + fqdnTemplate: "{{ .Name }}.{{ .Namespace }}.example.org", + expected: []*endpoint.Endpoint{ + {DNSName: "pod-1.default.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.67.94.101"}}, + {DNSName: "pod-2.kube-system.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.67.94.102"}}, + }, + }, + { + title: "templating with pod and multiple ips for types A and AAAA", + pods: []*v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-1", + Namespace: "default", + }, + Status: v1.PodStatus{ + PodIP: "100.67.94.101", + PodIPs: []v1.PodIP{ + {IP: "100.67.94.101"}, + {IP: "2041:0000:140F::875B:131B"}, + }, + }, + }, + }, + fqdnTemplate: "{{ .Name }}.example.org", + expected: []*endpoint.Endpoint{ + {DNSName: "pod-1.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.67.94.101"}}, + {DNSName: "pod-1.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2041:0000:140F::875B:131B"}}, + }, + }, + { + title: "templating with pod and target annotation that is currently not overriding target IPs", + pods: []*v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-1", + Namespace: "default", + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/target": "203.2.45.22", + }, + }, + Status: v1.PodStatus{ + PodIP: "100.67.94.101", + PodIPs: []v1.PodIP{ + {IP: "100.67.94.101"}, + }, + }, + }, + }, + fqdnTemplate: "{{ .Name }}.example.org", + expected: []*endpoint.Endpoint{ + {DNSName: "pod-1.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.67.94.101"}}, + }, + }, + { + title: "templating with pod and host annotation that is currently not overriding hostname", + pods: []*v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-1", + Namespace: "default", + Annotations: map[string]string{ + "external-dns.alpha.kubernetes.io/hostname": "ip-10-1-176-1.internal.domain.com", + }, + }, + Status: v1.PodStatus{ + PodIP: "100.67.94.101", + PodIPs: []v1.PodIP{ + {IP: "100.67.94.101"}, + }, + }, + }, + }, + fqdnTemplate: "{{ .Name }}.example.org", + expected: []*endpoint.Endpoint{ + {DNSName: "pod-1.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.67.94.101"}}, + }, + }, + { + title: "templating with simple annotation expansion", + pods: []*v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-1", + Namespace: "kube-system", + Annotations: map[string]string{ + "workload": "cluster-resources", + }, + }, + Status: v1.PodStatus{ + PodIP: "100.67.94.101", + PodIPs: []v1.PodIP{ + {IP: "100.67.94.101"}, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-2", + Namespace: "workloads", + Annotations: map[string]string{ + "workload": "workloads", + }, + }, + Status: v1.PodStatus{ + PodIP: "100.67.94.102", + PodIPs: []v1.PodIP{ + {IP: "100.67.94.102"}, + }, + }, + }, + }, + fqdnTemplate: "{{ .Name }}.{{ .Annotations.workload }}.domain.tld", + expected: []*endpoint.Endpoint{ + {DNSName: "pod-1.cluster-resources.domain.tld", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.67.94.101"}}, + {DNSName: "pod-2.workloads.domain.tld", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.67.94.102"}}, + }, + }, + { + title: "templating with complex label expansion", + pods: []*v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-1", + Namespace: "kube-system", + Labels: map[string]string{ + "topology.kubernetes.io/region": "eu-west-1a", + }, + }, + Status: v1.PodStatus{ + PodIP: "100.67.94.101", + PodIPs: []v1.PodIP{ + {IP: "100.67.94.101"}, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-2", + Namespace: "workloads", + Labels: map[string]string{ + "topology.kubernetes.io/region": "eu-west-1b", + }, + }, + Status: v1.PodStatus{ + PodIP: "100.67.94.102", + PodIPs: []v1.PodIP{ + {IP: "100.67.94.102"}, + }, + }, + }, + }, + fqdnTemplate: "{{ .Name }}.{{ index .ObjectMeta.Labels \"topology.kubernetes.io/region\" }}.domain.tld", + expected: []*endpoint.Endpoint{ + {DNSName: "pod-1.eu-west-1a.domain.tld", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.67.94.101"}}, + {DNSName: "pod-2.eu-west-1b.domain.tld", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.67.94.102"}}, + }, + }, + { + title: "templating with shared all domain", + pods: []*v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-1", + Namespace: "kube-system", + }, + Status: v1.PodStatus{ + PodIP: "100.67.94.101", + PodIPs: []v1.PodIP{ + {IP: "100.67.94.101"}, + {IP: "100.67.94.102"}, + {IP: "100.67.94.103"}, + {IP: "2041:0000:140F::875B:131B"}, + {IP: "::11.22.33.44"}, + }, + }, + }, + }, + fqdnTemplate: "pods-all.domain.tld", + expected: []*endpoint.Endpoint{ + {DNSName: "pods-all.domain.tld", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.67.94.101", "100.67.94.102", "100.67.94.103"}}, + {DNSName: "pods-all.domain.tld", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2041:0000:140F::875B:131B", "::11.22.33.44"}}, + }, + }, + { + title: "templating with fqdn template and IP not set as pod failed", + pods: []*v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-1", + Namespace: "kube-system", + }, + Status: v1.PodStatus{ + Phase: v1.PodRunning, + PodIP: "100.67.94.101", + PodIPs: []v1.PodIP{ + {IP: "100.67.94.101"}, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-2", + Namespace: "kube-system", + }, + Status: v1.PodStatus{ + Phase: v1.PodFailed, + }, + }, + }, + fqdnTemplate: "{{ .Name }}.domain.tld", + expected: []*endpoint.Endpoint{ + {DNSName: "pod-1.domain.tld", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.67.94.101"}}, + }, + }, + } { + t.Run(tt.title, func(t *testing.T) { + kubeClient := fake.NewClientset() + + for _, node := range tt.nodes { + _, err := kubeClient.CoreV1().Nodes().Create(t.Context(), node, metav1.CreateOptions{}) + require.NoError(t, err) + } + + for _, pod := range tt.pods { + _, err := kubeClient.CoreV1().Pods(pod.Namespace).Create(t.Context(), pod, metav1.CreateOptions{}) + require.NoError(t, err) + } + + src, err := NewPodSource( + t.Context(), + kubeClient, + "", + "", + false, + tt.sourceDomain, + tt.fqdnTemplate, + tt.combineFQDN) + require.NoError(t, err) + + endpoints, err := src.Endpoints(t.Context()) + require.NoError(t, err) + + validateEndpoints(t, endpoints, tt.expected) + }) + } +} + +func TestPodSourceFqdnTemplatingExamples_Failed(t *testing.T) { + for _, tt := range []struct { + title string + pods []*v1.Pod + nodes []*v1.Node + fqdnTemplate string + expected []*endpoint.Endpoint + combineFQDN bool + sourceDomain string + }{ + { + title: "templating with fqdn template correct but value does not exist", + pods: []*v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-1", + Namespace: "kube-system", + }, + Status: v1.PodStatus{ + PodIP: "100.67.94.101", + PodIPs: []v1.PodIP{ + {IP: "100.67.94.101"}, + }, + }, + }, + }, + fqdnTemplate: "{{ .Name }}.{{ .ThisNotExist }}.domain.tld", + expected: []*endpoint.Endpoint{}, + }, + } { + t.Run(tt.title, func(t *testing.T) { + kubeClient := fake.NewClientset() + + for _, node := range tt.nodes { + _, err := kubeClient.CoreV1().Nodes().Create(t.Context(), node, metav1.CreateOptions{}) + require.NoError(t, err) + } + + for _, pod := range tt.pods { + _, err := kubeClient.CoreV1().Pods(pod.Namespace).Create(t.Context(), pod, metav1.CreateOptions{}) + require.NoError(t, err) + } + + src, err := NewPodSource( + t.Context(), + kubeClient, + "", + "", + false, + tt.sourceDomain, + tt.fqdnTemplate, + tt.combineFQDN) + require.NoError(t, err) + + _, err = src.Endpoints(t.Context()) + require.Error(t, err) + }) + } +} diff --git a/source/pod_test.go b/source/pod_test.go index 1fb5243ab..65dedfa50 100644 --- a/source/pod_test.go +++ b/source/pod_test.go @@ -634,7 +634,7 @@ func TestPodSource(t *testing.T) { } { t.Run(tc.title, func(t *testing.T) { kubernetes := fake.NewClientset() - ctx := context.Background() + ctx := t.Context() // Create the nodes for _, node := range tc.nodes { @@ -652,7 +652,7 @@ func TestPodSource(t *testing.T) { } } - client, err := NewPodSource(context.TODO(), kubernetes, tc.targetNamespace, tc.compatibility, tc.ignoreNonHostNetworkPods, tc.PodSourceDomain) + client, err := NewPodSource(ctx, kubernetes, tc.targetNamespace, tc.compatibility, tc.ignoreNonHostNetworkPods, tc.PodSourceDomain, "", false) require.NoError(t, err) endpoints, err := client.Endpoints(ctx) @@ -664,6 +664,12 @@ func TestPodSource(t *testing.T) { // Validate returned endpoints against desired endpoints. validateEndpoints(t, endpoints, tc.expected) + + for _, ep := range endpoints { + // TODO: source should always set the resource label key. currently not supported by the pod source. + require.Empty(t, ep.Labels, "Labels should not be empty for endpoint %s", ep.DNSName) + require.NotContains(t, ep.Labels, endpoint.ResourceLabelKey) + } }) } } @@ -874,7 +880,7 @@ func TestPodSourceLogs(t *testing.T) { } } - client, err := NewPodSource(ctx, kubernetes, "", "", tc.ignoreNonHostNetworkPods, "") + client, err := NewPodSource(ctx, kubernetes, "", "", tc.ignoreNonHostNetworkPods, "", "", false) require.NoError(t, err) hook := testutils.LogsUnderTestWithLogLevel(log.DebugLevel, t) diff --git a/source/store.go b/source/store.go index f8ef39b58..f78664255 100644 --- a/source/store.go +++ b/source/store.go @@ -259,7 +259,7 @@ func ByNames(ctx context.Context, p ClientGenerator, names []string, cfg *Config return sources, nil } -// BuildWithConfig allows to generate a Source implementation from the shared config +// BuildWithConfig allows generating a Source implementation from the shared config func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg *Config) (Source, error) { switch source { case "node": @@ -285,7 +285,7 @@ func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg if err != nil { return nil, err } - return NewPodSource(ctx, client, cfg.Namespace, cfg.Compatibility, cfg.IgnoreNonHostNetworkPods, cfg.PodSourceDomain) + return NewPodSource(ctx, client, cfg.Namespace, cfg.Compatibility, cfg.IgnoreNonHostNetworkPods, cfg.PodSourceDomain, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation) case "gateway-httproute": return NewGatewayHTTPRouteSource(p, cfg) case "gateway-grpcroute": diff --git a/source/utils.go b/source/utils.go index 2dc04cf95..1023c3442 100644 --- a/source/utils.go +++ b/source/utils.go @@ -23,6 +23,7 @@ import ( // suitableType returns the DNS resource record type suitable for the target. // In this case type A/AAAA for IPs and type CNAME for everything else. +// TODO: move this to the endpoint package? func suitableType(target string) string { netIP, err := netip.ParseAddr(target) if err != nil {