feat(source/pod): add support for fqdn templating (#5512)

* feat(source/pod): add support for fqdn templating

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(source/pod): add support for fqdn templating

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(source/pod): add support for fqdn templating

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(source/pod): add support for fqdn templating

Co-authored-by: vflaux <38909103+vflaux@users.noreply.github.com>

* feat(source/pod): add support for fqdn templating

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(source/pod): add support for fqdn templating

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(source/pod): add support for fqdn templating

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* feat(source/pod): add support for fqdn templating

Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com>

---------

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
Co-authored-by: vflaux <38909103+vflaux@users.noreply.github.com>
Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com>
This commit is contained in:
Ivan Ka 2025-06-13 14:14:58 +01:00 committed by GitHub
parent 4d02fbe628
commit 7792986483
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 583 additions and 40 deletions

View File

@ -33,33 +33,33 @@ The template uses the following data from the source object (e.g., a `Service` o
<!-- TODO: generate from code -->
| 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

View File

@ -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 {

477
source/pod_fqdn_test.go Normal file
View File

@ -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)
})
}
}

View File

@ -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)

View File

@ -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":

View File

@ -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 {