mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 09:36:58 +02:00
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:
parent
4d02fbe628
commit
7792986483
@ -33,33 +33,33 @@ The template uses the following data from the source object (e.g., a `Service` o
|
|||||||
|
|
||||||
<!-- TODO: generate from code -->
|
<!-- TODO: generate from code -->
|
||||||
|
|
||||||
| Source | Description | FQDN Supported |
|
| Source | Description | FQDN Supported | FQDN Combine |
|
||||||
|:-----------------------|:----------------------------------------------------------------|:--------------:|
|
|:-----------------------|:----------------------------------------------------------------|:--------------:|:------------:|
|
||||||
| `ambassador-host` | Queries Ambassador Host resources for endpoints. | ❌ |
|
| `ambassador-host` | Queries Ambassador Host resources for endpoints. | ❌ | ❌ |
|
||||||
| `cloudfoundry` | Queries Cloud Foundry resources for endpoints. | ❌ |
|
| `cloudfoundry` | Queries Cloud Foundry resources for endpoints. | ❌ | ❌ |
|
||||||
| `connector` | Queries a custom connector source for endpoints. | ❌ |
|
| `connector` | Queries a custom connector source for endpoints. | ❌ | ❌ |
|
||||||
| `contour-httpproxy` | Queries Contour HTTPProxy resources for endpoints. | ✅ |
|
| `contour-httpproxy` | Queries Contour HTTPProxy resources for endpoints. | ✅ | ✅ |
|
||||||
| `crd` | Queries Custom Resource Definitions (CRDs) for endpoints. | ❌ |
|
| `crd` | Queries Custom Resource Definitions (CRDs) for endpoints. | ❌ | ❌ |
|
||||||
| `empty` | Uses an empty source, typically for testing or no-op scenarios. | ❌ |
|
| `empty` | Uses an empty source, typically for testing or no-op scenarios. | ❌ | ❌ |
|
||||||
| `f5-transportserver` | Queries F5 TransportServer resources for endpoints. | ❌ |
|
| `f5-transportserver` | Queries F5 TransportServer resources for endpoints. | ❌ | ❌ |
|
||||||
| `f5-virtualserver` | Queries F5 VirtualServer resources for endpoints. | ❌ |
|
| `f5-virtualserver` | Queries F5 VirtualServer resources for endpoints. | ❌ | ❌ |
|
||||||
| `fake` | Uses a fake source for testing purposes. | ❌ |
|
| `fake` | Uses a fake source for testing purposes. | ❌ | ❌ |
|
||||||
| `gateway-grpcroute` | Queries GRPCRoute resources from the Gateway API. | ✅ |
|
| `gateway-grpcroute` | Queries GRPCRoute resources from the Gateway API. | ✅ | ❌ |
|
||||||
| `gateway-httproute` | Queries HTTPRoute resources from the Gateway API. | ✅ |
|
| `gateway-httproute` | Queries HTTPRoute resources from the Gateway API. | ✅ | ❌ |
|
||||||
| `gateway-tcproute` | Queries TCPRoute resources from the Gateway API. | ✅ |
|
| `gateway-tcproute` | Queries TCPRoute resources from the Gateway API. | ✅ | ❌ |
|
||||||
| `gateway-tlsroute` | Queries TLSRoute resources from the Gateway API. | ❌ |
|
| `gateway-tlsroute` | Queries TLSRoute resources from the Gateway API. | ❌ | ❌ |
|
||||||
| `gateway-udproute` | Queries UDPRoute resources from the Gateway API. | ❌ |
|
| `gateway-udproute` | Queries UDPRoute resources from the Gateway API. | ❌ | ❌ |
|
||||||
| `gloo-proxy` | Queries Gloo Proxy resources for endpoints. | ❌ |
|
| `gloo-proxy` | Queries Gloo Proxy resources for endpoints. | ❌ | ❌ |
|
||||||
| `ingress` | Queries Kubernetes Ingress resources for endpoints. | ✅ |
|
| `ingress` | Queries Kubernetes Ingress resources for endpoints. | ✅ | ✅ |
|
||||||
| `istio-gateway` | Queries Istio Gateway resources for endpoints. | ✅ |
|
| `istio-gateway` | Queries Istio Gateway resources for endpoints. | ✅ | ✅ |
|
||||||
| `istio-virtualservice` | Queries Istio VirtualService resources for endpoints. | ✅ |
|
| `istio-virtualservice` | Queries Istio VirtualService resources for endpoints. | ✅ | ✅ |
|
||||||
| `kong-tcpingress` | Queries Kong TCPIngress resources for endpoints. | ❌ |
|
| `kong-tcpingress` | Queries Kong TCPIngress resources for endpoints. | ❌ | ❌ |
|
||||||
| `node` | Queries Kubernetes Node resources for endpoints. | ✅ |
|
| `node` | Queries Kubernetes Node resources for endpoints. | ✅ | ❌ |
|
||||||
| `openshift-route` | Queries OpenShift Route resources for endpoints. | ✅ |
|
| `openshift-route` | Queries OpenShift Route resources for endpoints. | ✅ | ✅ |
|
||||||
| `pod` | Queries Kubernetes Pod resources for endpoints. | ❌ |
|
| `pod` | Queries Kubernetes Pod resources for endpoints. | ✅ | ✅ |
|
||||||
| `service` | Queries Kubernetes Service resources for endpoints. | ✅ |
|
| `service` | Queries Kubernetes Service resources for endpoints. | ✅ | ✅ |
|
||||||
| `skipper-routegroup` | Queries Skipper RouteGroup resources for endpoints. | ✅ |
|
| `skipper-routegroup` | Queries Skipper RouteGroup resources for endpoints. | ✅ | ✅ |
|
||||||
| `traefik-proxy` | Queries Traefik Proxy resources for endpoints. | ❌ |
|
| `traefik-proxy` | Queries Traefik IngressRoute resources for endpoints. | ❌ | ❌ |
|
||||||
|
|
||||||
## Custom Functions
|
## Custom Functions
|
||||||
|
|
||||||
|
@ -18,6 +18,9 @@ package source
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
@ -27,14 +30,19 @@ import (
|
|||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/client-go/tools/cache"
|
"k8s.io/client-go/tools/cache"
|
||||||
|
|
||||||
|
"sigs.k8s.io/external-dns/source/fqdn"
|
||||||
|
|
||||||
"sigs.k8s.io/external-dns/endpoint"
|
"sigs.k8s.io/external-dns/endpoint"
|
||||||
"sigs.k8s.io/external-dns/source/annotations"
|
"sigs.k8s.io/external-dns/source/annotations"
|
||||||
"sigs.k8s.io/external-dns/source/informers"
|
"sigs.k8s.io/external-dns/source/informers"
|
||||||
)
|
)
|
||||||
|
|
||||||
type podSource struct {
|
type podSource struct {
|
||||||
client kubernetes.Interface
|
client kubernetes.Interface
|
||||||
namespace string
|
namespace string
|
||||||
|
fqdnTemplate *template.Template
|
||||||
|
combineFQDNAnnotation bool
|
||||||
|
|
||||||
podInformer coreinformers.PodInformer
|
podInformer coreinformers.PodInformer
|
||||||
nodeInformer coreinformers.NodeInformer
|
nodeInformer coreinformers.NodeInformer
|
||||||
compatibility string
|
compatibility string
|
||||||
@ -43,18 +51,27 @@ type podSource struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewPodSource creates a new podSource with the given config.
|
// 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))
|
informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(kubeClient, 0, kubeinformers.WithNamespace(namespace))
|
||||||
podInformer := informerFactory.Core().V1().Pods()
|
podInformer := informerFactory.Core().V1().Pods()
|
||||||
nodeInformer := informerFactory.Core().V1().Nodes()
|
nodeInformer := informerFactory.Core().V1().Nodes()
|
||||||
|
|
||||||
podInformer.Informer().AddEventHandler(
|
_, _ = podInformer.Informer().AddEventHandler(
|
||||||
cache.ResourceEventHandlerFuncs{
|
cache.ResourceEventHandlerFuncs{
|
||||||
AddFunc: func(obj interface{}) {
|
AddFunc: func(obj interface{}) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
nodeInformer.Informer().AddEventHandler(
|
_, _ = nodeInformer.Informer().AddEventHandler(
|
||||||
cache.ResourceEventHandlerFuncs{
|
cache.ResourceEventHandlerFuncs{
|
||||||
AddFunc: func(obj interface{}) {
|
AddFunc: func(obj interface{}) {
|
||||||
},
|
},
|
||||||
@ -68,6 +85,11 @@ func NewPodSource(ctx context.Context, kubeClient kubernetes.Interface, namespac
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tmpl, err := fqdn.ParseTemplate(fqdnTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &podSource{
|
return &podSource{
|
||||||
client: kubeClient,
|
client: kubeClient,
|
||||||
podInformer: podInformer,
|
podInformer: podInformer,
|
||||||
@ -76,13 +98,15 @@ func NewPodSource(ctx context.Context, kubeClient kubernetes.Interface, namespac
|
|||||||
compatibility: compatibility,
|
compatibility: compatibility,
|
||||||
ignoreNonHostNetworkPods: ignoreNonHostNetworkPods,
|
ignoreNonHostNetworkPods: ignoreNonHostNetworkPods,
|
||||||
podSourceDomain: podSourceDomain,
|
podSourceDomain: podSourceDomain,
|
||||||
|
fqdnTemplate: tmpl,
|
||||||
|
combineFQDNAnnotation: combineFqdnAnnotation,
|
||||||
}, nil
|
}, 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())
|
pods, err := ps.podInformer.Lister().Pods(ps.namespace).List(labels.Everything())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -90,8 +114,19 @@ func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
|
|||||||
|
|
||||||
endpointMap := make(map[endpoint.EndpointKey][]string)
|
endpointMap := make(map[endpoint.EndpointKey][]string)
|
||||||
for _, pod := range pods {
|
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
|
var endpoints []*endpoint.Endpoint
|
||||||
for key, targets := range endpointMap {
|
for key, targets := range endpointMap {
|
||||||
endpoints = append(endpoints, endpoint.NewEndpoint(key.DNSName, key.RecordType, targets...))
|
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) {
|
func addTargetsToEndpointMap(endpointMap map[endpoint.EndpointKey][]string, targets []string, domainList ...string) {
|
||||||
for _, domain := range domainList {
|
for _, domain := range domainList {
|
||||||
for _, target := range targets {
|
for _, target := range targets {
|
||||||
|
477
source/pod_fqdn_test.go
Normal file
477
source/pod_fqdn_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -634,7 +634,7 @@ func TestPodSource(t *testing.T) {
|
|||||||
} {
|
} {
|
||||||
t.Run(tc.title, func(t *testing.T) {
|
t.Run(tc.title, func(t *testing.T) {
|
||||||
kubernetes := fake.NewClientset()
|
kubernetes := fake.NewClientset()
|
||||||
ctx := context.Background()
|
ctx := t.Context()
|
||||||
|
|
||||||
// Create the nodes
|
// Create the nodes
|
||||||
for _, node := range tc.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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
endpoints, err := client.Endpoints(ctx)
|
endpoints, err := client.Endpoints(ctx)
|
||||||
@ -664,6 +664,12 @@ func TestPodSource(t *testing.T) {
|
|||||||
|
|
||||||
// Validate returned endpoints against desired endpoints.
|
// Validate returned endpoints against desired endpoints.
|
||||||
validateEndpoints(t, endpoints, tc.expected)
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
hook := testutils.LogsUnderTestWithLogLevel(log.DebugLevel, t)
|
hook := testutils.LogsUnderTestWithLogLevel(log.DebugLevel, t)
|
||||||
|
@ -259,7 +259,7 @@ func ByNames(ctx context.Context, p ClientGenerator, names []string, cfg *Config
|
|||||||
return sources, nil
|
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) {
|
func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg *Config) (Source, error) {
|
||||||
switch source {
|
switch source {
|
||||||
case "node":
|
case "node":
|
||||||
@ -285,7 +285,7 @@ func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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":
|
case "gateway-httproute":
|
||||||
return NewGatewayHTTPRouteSource(p, cfg)
|
return NewGatewayHTTPRouteSource(p, cfg)
|
||||||
case "gateway-grpcroute":
|
case "gateway-grpcroute":
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
|
|
||||||
// suitableType returns the DNS resource record type suitable for the target.
|
// 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.
|
// 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 {
|
func suitableType(target string) string {
|
||||||
netIP, err := netip.ParseAddr(target)
|
netIP, err := netip.ParseAddr(target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Loading…
Reference in New Issue
Block a user