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 -->
|
||||
|
||||
| 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
|
||||
|
||||
|
@ -18,6 +18,9 @@ package source
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"text/template"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@ -27,6 +30,8 @@ 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"
|
||||
@ -35,6 +40,9 @@ import (
|
||||
type podSource struct {
|
||||
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 {
|
||||
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
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) {
|
||||
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)
|
||||
|
@ -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":
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user