Skip to content

Commit 11db843

Browse files
author
Sergey Kolosov
committed
feat: add new flag to allow configuring per pod FQDNs for service source.
1 parent 8fd9c64 commit 11db843

File tree

6 files changed

+135
-3
lines changed

6 files changed

+135
-3
lines changed

docs/flags.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,5 +180,6 @@
180180
| `--webhook-provider-read-timeout=5s` | The read timeout for the webhook provider in duration format (default: 5s) |
181181
| `--webhook-provider-write-timeout=10s` | The write timeout for the webhook provider in duration format (default: 10s) |
182182
| `--[no-]webhook-server` | When enabled, runs as a webhook server instead of a controller. (default: false). |
183+
| `--service-per-pod-fqdn=` | enables/disables to create per pod FQDNs for headless services. (default: 'true' for pods with non empty Hostnames, 'false' otherwise). |
183184
| `--provider=provider` | The DNS provider where the DNS records will be created (required, options: akamai, alibabacloud, aws, aws-sd, azure, azure-dns, azure-private-dns, civo, cloudflare, coredns, digitalocean, dnsimple, exoscale, gandi, godaddy, google, inmemory, linode, ns1, oci, ovh, pdns, pihole, plural, rfc2136, scaleway, skydns, transip, webhook) |
184185
| `--source=source` | The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, pod, fake, connector, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, istio-gateway, istio-virtualservice, cloudfoundry, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress, f5-virtualserver, f5-transportserver, traefik-proxy) |

pkg/apis/externaldns/types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ type Config struct {
217217
ExcludeUnschedulable bool
218218
EmitEvents []string
219219
ForceDefaultTargets bool
220+
ServicePerPodFqdn string
220221
}
221222

222223
var defaultConfig = &Config{
@@ -382,6 +383,7 @@ var defaultConfig = &Config{
382383
WebhookServer: false,
383384
ZoneIDFilter: []string{},
384385
ForceDefaultTargets: false,
386+
ServicePerPodFqdn: "",
385387
}
386388

387389
var providerNames = []string{
@@ -804,6 +806,7 @@ func bindFlags(b FlagBinder, cfg *Config) {
804806
b.DurationVar("webhook-provider-read-timeout", "The read timeout for the webhook provider in duration format (default: 5s)", defaultConfig.WebhookProviderReadTimeout, &cfg.WebhookProviderReadTimeout)
805807
b.DurationVar("webhook-provider-write-timeout", "The write timeout for the webhook provider in duration format (default: 10s)", defaultConfig.WebhookProviderWriteTimeout, &cfg.WebhookProviderWriteTimeout)
806808
b.BoolVar("webhook-server", "When enabled, runs as a webhook server instead of a controller. (default: false).", defaultConfig.WebhookServer, &cfg.WebhookServer)
809+
b.EnumVar("service-per-pod-fqdn", "enables/disables to create per pod FQDNs for headless services. (default: 'true' for pods with non empty Hostnames, 'false' otherwise).", defaultConfig.ServicePerPodFqdn, &cfg.ServicePerPodFqdn, "true", "false")
807810
}
808811

809812
func App(cfg *Config) *kingpin.Application {

source/service.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ type serviceSource struct {
7575
alwaysPublishNotReadyAddresses bool
7676
resolveLoadBalancerHostname bool
7777
listenEndpointEvents bool
78+
servicePerPodFqdn *bool
7879
serviceInformer coreinformers.ServiceInformer
7980
endpointSlicesInformer discoveryinformers.EndpointSliceInformer
8081
podInformer coreinformers.PodInformer
@@ -97,7 +98,7 @@ func NewServiceSource(
9798
ignoreHostnameAnnotation bool,
9899
labelSelector labels.Selector,
99100
resolveLoadBalancerHostname,
100-
listenEndpointEvents, exposeInternalIPv6 bool,
101+
listenEndpointEvents, exposeInternalIPv6 bool, servicePerPodFqdn *bool,
101102
) (Source, error) {
102103
tmpl, err := fqdn.ParseTemplate(fqdnTemplate)
103104
if err != nil {
@@ -223,6 +224,7 @@ func NewServiceSource(
223224
resolveLoadBalancerHostname: resolveLoadBalancerHostname,
224225
listenEndpointEvents: listenEndpointEvents,
225226
exposeInternalIPv6: exposeInternalIPv6,
227+
servicePerPodFqdn: servicePerPodFqdn,
226228
}, nil
227229
}
228230

@@ -410,8 +412,10 @@ func (sc *serviceSource) processHeadlessEndpointsFromSlices(
410412
continue
411413
}
412414
headlessDomains := []string{hostname}
413-
if pod.Spec.Hostname != "" {
415+
if pod.Spec.Hostname != "" && (sc.servicePerPodFqdn == nil || *sc.servicePerPodFqdn) {
414416
headlessDomains = append(headlessDomains, fmt.Sprintf("%s.%s", pod.Spec.Hostname, hostname))
417+
} else if sc.servicePerPodFqdn != nil && *sc.servicePerPodFqdn {
418+
headlessDomains = append(headlessDomains, fmt.Sprintf("%s.%s", pod.Name, hostname))
415419
}
416420
for _, headlessDomain := range headlessDomains {
417421
targets := sc.getTargetsForDomain(pod, ep, endpointSlice, endpointsType, headlessDomain)

source/service_fqdn_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,7 @@ func TestServiceSourceFqdnTemplatingExamples(t *testing.T) {
745745
false,
746746
false,
747747
true,
748+
nil,
748749
)
749750
require.NoError(t, err)
750751

source/service_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"maps"
2323
"math/rand"
2424
"net"
25+
"slices"
2526
"sort"
2627
"strings"
2728
"testing"
@@ -38,6 +39,7 @@ import (
3839
"k8s.io/apimachinery/pkg/util/intstr"
3940
kubeinformers "k8s.io/client-go/informers"
4041
"k8s.io/client-go/kubernetes/fake"
42+
"k8s.io/utils/ptr"
4143
"sigs.k8s.io/external-dns/endpoint"
4244
"sigs.k8s.io/external-dns/internal/testutils"
4345
"sigs.k8s.io/external-dns/source/annotations"
@@ -91,6 +93,7 @@ func (suite *ServiceSuite) SetupTest() {
9193
false,
9294
false,
9395
false,
96+
nil,
9497
)
9598
suite.NoError(err, "should initialize service source")
9699
}
@@ -174,6 +177,7 @@ func testServiceSourceNewServiceSource(t *testing.T) {
174177
false,
175178
false,
176179
false,
180+
nil,
177181
)
178182

179183
if ti.expectError {
@@ -1158,6 +1162,7 @@ func testServiceSourceEndpoints(t *testing.T) {
11581162
tc.resolveLoadBalancerHostname,
11591163
false,
11601164
false,
1165+
nil,
11611166
)
11621167

11631168
require.NoError(t, err)
@@ -1374,6 +1379,7 @@ func testMultipleServicesEndpoints(t *testing.T) {
13741379
false,
13751380
false,
13761381
false,
1382+
nil,
13771383
)
13781384
require.NoError(t, err)
13791385

@@ -1679,6 +1685,7 @@ func TestClusterIpServices(t *testing.T) {
16791685
false,
16801686
false,
16811687
false,
1688+
nil,
16821689
)
16831690
require.NoError(t, err)
16841691

@@ -2456,6 +2463,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
24562463
false,
24572464
false,
24582465
tc.exposeInternalIPv6,
2466+
nil,
24592467
)
24602468
require.NoError(t, err)
24612469

@@ -3364,6 +3372,7 @@ func TestHeadlessServices(t *testing.T) {
33643372
false,
33653373
false,
33663374
tc.exposeInternalIPv6,
3375+
nil,
33673376
)
33683377
require.NoError(t, err)
33693378

@@ -3500,6 +3509,7 @@ func TestMultipleServicesPointingToSameLoadBalancer(t *testing.T) {
35003509
false,
35013510
false,
35023511
false,
3512+
nil,
35033513
)
35043514
require.NoError(t, err)
35053515
assert.NotNil(t, src)
@@ -3866,6 +3876,7 @@ func TestMultipleHeadlessServicesPointingToPodsOnTheSameNode(t *testing.T) {
38663876
false,
38673877
false,
38683878
false,
3879+
nil,
38693880
)
38703881
require.NoError(t, err)
38713882
assert.NotNil(t, src)
@@ -4324,6 +4335,7 @@ func TestHeadlessServicesHostIP(t *testing.T) {
43244335
false,
43254336
false,
43264337
false,
4338+
nil,
43274339
)
43284340
require.NoError(t, err)
43294341

@@ -4534,6 +4546,7 @@ func TestExternalServices(t *testing.T) {
45344546
false,
45354547
false,
45364548
false,
4549+
nil,
45374550
)
45384551
require.NoError(t, err)
45394552

@@ -4596,6 +4609,7 @@ func BenchmarkServiceEndpoints(b *testing.B) {
45964609
false,
45974610
false,
45984611
false,
4612+
nil,
45994613
)
46004614
require.NoError(b, err)
46014615

@@ -4695,6 +4709,7 @@ func TestNewServiceSourceInformersEnabled(t *testing.T) {
46954709
false,
46964710
false,
46974711
false,
4712+
nil,
46984713
)
46994714
require.NoError(t, err)
47004715
svcSrc, ok := svc.(*serviceSource)
@@ -4726,6 +4741,7 @@ func TestNewServiceSourceWithServiceTypeFilters_Unsupported(t *testing.T) {
47264741
false,
47274742
false,
47284743
false,
4744+
nil,
47294745
)
47304746
require.Errorf(t, err, "unsupported service type filter: \"UnknownType\". Supported types are: [\"ClusterIP\" \"NodePort\" \"LoadBalancer\" \"ExternalName\"]")
47314747
require.Nil(t, svc, "ServiceSource should be nil when an unsupported service type is provided")
@@ -4905,6 +4921,7 @@ func TestEndpointSlicesIndexer(t *testing.T) {
49054921
false,
49064922
false,
49074923
false,
4924+
nil,
49084925
)
49094926
require.NoError(t, err)
49104927
ss, ok := src.(*serviceSource)
@@ -4992,6 +5009,7 @@ func TestPodTransformerInServiceSource(t *testing.T) {
49925009
false,
49935010
false,
49945011
false,
5012+
nil,
49955013
)
49965014
require.NoError(t, err)
49975015
ss, ok := src.(*serviceSource)
@@ -5356,6 +5374,96 @@ func TestProcessEndpointSlices_NotReadyWithPublishNotReady(t *testing.T) {
53565374
assert.NotEmpty(t, result, "Not ready endpoints should be processed when publishNotReadyAddresses is true")
53575375
}
53585376

5377+
func TestProcessEndpointSlices_PerPodFQDN(t *testing.T) {
5378+
for _, test := range []struct {
5379+
title string
5380+
servicePerPodFqdn *bool
5381+
podName string
5382+
hostName string
5383+
expectedFqdnPrefix string
5384+
}{
5385+
{
5386+
title: "pod's FQDN should be created if hostname is specified",
5387+
servicePerPodFqdn: nil,
5388+
podName: "test-pod-name",
5389+
hostName: "test-pod-host-name",
5390+
expectedFqdnPrefix: "test-pod-host-name",
5391+
},
5392+
{
5393+
title: "pod's FQDN should be created if enabled",
5394+
servicePerPodFqdn: ptr.To(true),
5395+
podName: "test-pod-name",
5396+
hostName: "",
5397+
expectedFqdnPrefix: "test-pod-name",
5398+
},
5399+
{
5400+
title: "pod's FQDN should not be created if hostname is specified and disabled",
5401+
servicePerPodFqdn: ptr.To(false),
5402+
podName: "test-pod-name",
5403+
hostName: "test-pod-host-name",
5404+
expectedFqdnPrefix: "",
5405+
},
5406+
{
5407+
title: "pod's FQDN should not be created if disabled",
5408+
servicePerPodFqdn: ptr.To(false),
5409+
podName: "test-pod-name",
5410+
hostName: "",
5411+
expectedFqdnPrefix: "",
5412+
},
5413+
{
5414+
title: "hostname should be used for pod's FQDN",
5415+
servicePerPodFqdn: ptr.To(true),
5416+
podName: "test-pod-name",
5417+
hostName: "test-pod-host-name",
5418+
expectedFqdnPrefix: "test-pod-host-name",
5419+
},
5420+
} {
5421+
t.Run(test.title, func(t *testing.T) {
5422+
sc := &serviceSource{servicePerPodFqdn: test.servicePerPodFqdn}
5423+
svc := &v1.Service{
5424+
ObjectMeta: metav1.ObjectMeta{Name: "test-service", Namespace: "default"},
5425+
}
5426+
5427+
endpointSlice := &discoveryv1.EndpointSlice{
5428+
ObjectMeta: metav1.ObjectMeta{Name: "slice1", Namespace: "default"},
5429+
AddressType: discoveryv1.AddressTypeIPv4,
5430+
Endpoints: []discoveryv1.Endpoint{
5431+
{
5432+
TargetRef: &v1.ObjectReference{Kind: "Pod", Name: test.podName},
5433+
Conditions: discoveryv1.EndpointConditions{Ready: testutils.ToPtr(false)}, // Not ready
5434+
Addresses: []string{"10.0.0.1"},
5435+
},
5436+
},
5437+
}
5438+
pods := []*v1.Pod{{
5439+
ObjectMeta: metav1.ObjectMeta{Name: test.podName},
5440+
Status: v1.PodStatus{PodIP: "10.0.0.1"},
5441+
Spec: v1.PodSpec{Hostname: test.hostName},
5442+
}}
5443+
const serviceHostname = "test-service.example.com"
5444+
const endpointsType = "IPv4"
5445+
const publishPodIPs = false
5446+
const publishNotReadyAddresses = true // This should allow not-ready endpoints
5447+
5448+
result := sc.processHeadlessEndpointsFromSlices(
5449+
svc, pods, []*discoveryv1.EndpointSlice{endpointSlice},
5450+
serviceHostname, endpointsType, publishPodIPs, publishNotReadyAddresses)
5451+
if len(test.expectedFqdnPrefix) > 0 {
5452+
expectedPodFqdn := test.expectedFqdnPrefix + "." + serviceHostname
5453+
hasPodFqdn := slices.ContainsFunc(slices.Collect(maps.Keys(result)), func(record endpoint.EndpointKey) bool {
5454+
return record.DNSName == expectedPodFqdn
5455+
})
5456+
assert.True(t, hasPodFqdn, "Endpoint with pod's hostname (%s) should be generated but got: %v", expectedPodFqdn, result)
5457+
} else {
5458+
hasServiceFqdn := slices.ContainsFunc(slices.Collect(maps.Keys(result)), func(record endpoint.EndpointKey) bool {
5459+
return record.DNSName == serviceHostname
5460+
})
5461+
assert.True(t, hasServiceFqdn && len(result) == 1, "Result should include only service endpoint (%s): %v", serviceHostname, result)
5462+
}
5463+
})
5464+
}
5465+
}
5466+
53595467
// Test getTargetsForDomain with empty ep.Addresses
53605468
func TestGetTargetsForDomain_EmptyAddresses(t *testing.T) {
53615469
sc := &serviceSource{}

source/store.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"errors"
2222
"fmt"
2323
"os"
24+
"strings"
2425
"sync"
2526
"time"
2627

@@ -33,6 +34,7 @@ import (
3334
"k8s.io/client-go/kubernetes"
3435
"k8s.io/client-go/rest"
3536
"k8s.io/client-go/tools/clientcmd"
37+
"k8s.io/utils/ptr"
3638
gateway "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned"
3739

3840
"sigs.k8s.io/external-dns/source/types"
@@ -101,6 +103,7 @@ type Config struct {
101103
TraefikDisableNew bool
102104
ExcludeUnschedulable bool
103105
ExposeInternalIPv6 bool
106+
ServicePerPodFqdn *bool
104107
}
105108

106109
func NewSourceConfig(cfg *externaldns.Config) *Config {
@@ -147,6 +150,18 @@ func NewSourceConfig(cfg *externaldns.Config) *Config {
147150
TraefikDisableNew: cfg.TraefikDisableNew,
148151
ExcludeUnschedulable: cfg.ExcludeUnschedulable,
149152
ExposeInternalIPv6: cfg.ExposeInternalIPV6,
153+
ServicePerPodFqdn: toBoolPtr(cfg.ServicePerPodFqdn),
154+
}
155+
}
156+
157+
func toBoolPtr(value string) *bool {
158+
if len(value) == 0 {
159+
return nil
160+
}
161+
if strings.EqualFold(value, "true") {
162+
return ptr.To(true)
163+
} else {
164+
return ptr.To(false)
150165
}
151166
}
152167

@@ -429,7 +444,7 @@ func buildServiceSource(ctx context.Context, p ClientGenerator, cfg *Config) (So
429444
if err != nil {
430445
return nil, err
431446
}
432-
return NewServiceSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.ResolveLoadBalancerHostname, cfg.ListenEndpointEvents, cfg.ExposeInternalIPv6)
447+
return NewServiceSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.ResolveLoadBalancerHostname, cfg.ListenEndpointEvents, cfg.ExposeInternalIPv6, cfg.ServicePerPodFqdn)
433448
}
434449

435450
// buildIngressSource creates an Ingress source for exposing Kubernetes ingresses as DNS records.

0 commit comments

Comments
 (0)