Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,5 +180,6 @@
| `--webhook-provider-read-timeout=5s` | The read timeout for the webhook provider in duration format (default: 5s) |
| `--webhook-provider-write-timeout=10s` | The write timeout for the webhook provider in duration format (default: 10s) |
| `--[no-]webhook-server` | When enabled, runs as a webhook server instead of a controller. (default: false). |
| `--service-per-pod-fqdn=` | enables/disables to create per pod FQDNs for headless services. (default: 'true' for pods with non empty Hostnames, 'false' otherwise). |
| `--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) |
| `--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) |
3 changes: 3 additions & 0 deletions pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ type Config struct {
ExcludeUnschedulable bool
EmitEvents []string
ForceDefaultTargets bool
ServicePerPodFqdn string
}

var defaultConfig = &Config{
Expand Down Expand Up @@ -382,6 +383,7 @@ var defaultConfig = &Config{
WebhookServer: false,
ZoneIDFilter: []string{},
ForceDefaultTargets: false,
ServicePerPodFqdn: "",
}

var providerNames = []string{
Expand Down Expand Up @@ -804,6 +806,7 @@ func bindFlags(b FlagBinder, cfg *Config) {
b.DurationVar("webhook-provider-read-timeout", "The read timeout for the webhook provider in duration format (default: 5s)", defaultConfig.WebhookProviderReadTimeout, &cfg.WebhookProviderReadTimeout)
b.DurationVar("webhook-provider-write-timeout", "The write timeout for the webhook provider in duration format (default: 10s)", defaultConfig.WebhookProviderWriteTimeout, &cfg.WebhookProviderWriteTimeout)
b.BoolVar("webhook-server", "When enabled, runs as a webhook server instead of a controller. (default: false).", defaultConfig.WebhookServer, &cfg.WebhookServer)
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", "")
}

func App(cfg *Config) *kingpin.Application {
Expand Down
8 changes: 6 additions & 2 deletions source/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ type serviceSource struct {
alwaysPublishNotReadyAddresses bool
resolveLoadBalancerHostname bool
listenEndpointEvents bool
servicePerPodFqdn *bool
serviceInformer coreinformers.ServiceInformer
endpointSlicesInformer discoveryinformers.EndpointSliceInformer
podInformer coreinformers.PodInformer
Expand All @@ -97,7 +98,7 @@ func NewServiceSource(
ignoreHostnameAnnotation bool,
labelSelector labels.Selector,
resolveLoadBalancerHostname,
listenEndpointEvents, exposeInternalIPv6 bool,
listenEndpointEvents, exposeInternalIPv6 bool, servicePerPodFqdn *bool,
) (Source, error) {
tmpl, err := fqdn.ParseTemplate(fqdnTemplate)
if err != nil {
Expand Down Expand Up @@ -223,6 +224,7 @@ func NewServiceSource(
resolveLoadBalancerHostname: resolveLoadBalancerHostname,
listenEndpointEvents: listenEndpointEvents,
exposeInternalIPv6: exposeInternalIPv6,
servicePerPodFqdn: servicePerPodFqdn,
}, nil
}

Expand Down Expand Up @@ -410,8 +412,10 @@ func (sc *serviceSource) processHeadlessEndpointsFromSlices(
continue
}
headlessDomains := []string{hostname}
if pod.Spec.Hostname != "" {
if pod.Spec.Hostname != "" && (sc.servicePerPodFqdn == nil || *sc.servicePerPodFqdn) {
headlessDomains = append(headlessDomains, fmt.Sprintf("%s.%s", pod.Spec.Hostname, hostname))
} else if sc.servicePerPodFqdn != nil && *sc.servicePerPodFqdn {
headlessDomains = append(headlessDomains, fmt.Sprintf("%s.%s", pod.Name, hostname))
}
for _, headlessDomain := range headlessDomains {
targets := sc.getTargetsForDomain(pod, ep, endpointSlice, endpointsType, headlessDomain)
Expand Down
1 change: 1 addition & 0 deletions source/service_fqdn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,7 @@ func TestServiceSourceFqdnTemplatingExamples(t *testing.T) {
false,
false,
true,
nil,
)
require.NoError(t, err)

Expand Down
108 changes: 108 additions & 0 deletions source/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"maps"
"math/rand"
"net"
"slices"
"sort"
"strings"
"testing"
Expand All @@ -38,6 +39,7 @@ import (
"k8s.io/apimachinery/pkg/util/intstr"
kubeinformers "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/utils/ptr"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/source/annotations"
Expand Down Expand Up @@ -91,6 +93,7 @@ func (suite *ServiceSuite) SetupTest() {
false,
false,
false,
nil,
)
suite.NoError(err, "should initialize service source")
}
Expand Down Expand Up @@ -174,6 +177,7 @@ func testServiceSourceNewServiceSource(t *testing.T) {
false,
false,
false,
nil,
)

if ti.expectError {
Expand Down Expand Up @@ -1158,6 +1162,7 @@ func testServiceSourceEndpoints(t *testing.T) {
tc.resolveLoadBalancerHostname,
false,
false,
nil,
)

require.NoError(t, err)
Expand Down Expand Up @@ -1374,6 +1379,7 @@ func testMultipleServicesEndpoints(t *testing.T) {
false,
false,
false,
nil,
)
require.NoError(t, err)

Expand Down Expand Up @@ -1679,6 +1685,7 @@ func TestClusterIpServices(t *testing.T) {
false,
false,
false,
nil,
)
require.NoError(t, err)

Expand Down Expand Up @@ -2456,6 +2463,7 @@ func TestServiceSourceNodePortServices(t *testing.T) {
false,
false,
tc.exposeInternalIPv6,
nil,
)
require.NoError(t, err)

Expand Down Expand Up @@ -3364,6 +3372,7 @@ func TestHeadlessServices(t *testing.T) {
false,
false,
tc.exposeInternalIPv6,
nil,
)
require.NoError(t, err)

Expand Down Expand Up @@ -3500,6 +3509,7 @@ func TestMultipleServicesPointingToSameLoadBalancer(t *testing.T) {
false,
false,
false,
nil,
)
require.NoError(t, err)
assert.NotNil(t, src)
Expand Down Expand Up @@ -3866,6 +3876,7 @@ func TestMultipleHeadlessServicesPointingToPodsOnTheSameNode(t *testing.T) {
false,
false,
false,
nil,
)
require.NoError(t, err)
assert.NotNil(t, src)
Expand Down Expand Up @@ -4324,6 +4335,7 @@ func TestHeadlessServicesHostIP(t *testing.T) {
false,
false,
false,
nil,
)
require.NoError(t, err)

Expand Down Expand Up @@ -4534,6 +4546,7 @@ func TestExternalServices(t *testing.T) {
false,
false,
false,
nil,
)
require.NoError(t, err)

Expand Down Expand Up @@ -4596,6 +4609,7 @@ func BenchmarkServiceEndpoints(b *testing.B) {
false,
false,
false,
nil,
)
require.NoError(b, err)

Expand Down Expand Up @@ -4695,6 +4709,7 @@ func TestNewServiceSourceInformersEnabled(t *testing.T) {
false,
false,
false,
nil,
)
require.NoError(t, err)
svcSrc, ok := svc.(*serviceSource)
Expand Down Expand Up @@ -4726,6 +4741,7 @@ func TestNewServiceSourceWithServiceTypeFilters_Unsupported(t *testing.T) {
false,
false,
false,
nil,
)
require.Errorf(t, err, "unsupported service type filter: \"UnknownType\". Supported types are: [\"ClusterIP\" \"NodePort\" \"LoadBalancer\" \"ExternalName\"]")
require.Nil(t, svc, "ServiceSource should be nil when an unsupported service type is provided")
Expand Down Expand Up @@ -4905,6 +4921,7 @@ func TestEndpointSlicesIndexer(t *testing.T) {
false,
false,
false,
nil,
)
require.NoError(t, err)
ss, ok := src.(*serviceSource)
Expand Down Expand Up @@ -4992,6 +5009,7 @@ func TestPodTransformerInServiceSource(t *testing.T) {
false,
false,
false,
nil,
)
require.NoError(t, err)
ss, ok := src.(*serviceSource)
Expand Down Expand Up @@ -5356,6 +5374,96 @@ func TestProcessEndpointSlices_NotReadyWithPublishNotReady(t *testing.T) {
assert.NotEmpty(t, result, "Not ready endpoints should be processed when publishNotReadyAddresses is true")
}

func TestProcessEndpointSlices_PerPodFQDN(t *testing.T) {
for _, test := range []struct {
title string
servicePerPodFqdn *bool
podName string
hostName string
expectedFqdnPrefix string
}{
{
title: "pod's FQDN should be created if hostname is specified",
servicePerPodFqdn: nil,
podName: "test-pod-name",
hostName: "test-pod-host-name",
expectedFqdnPrefix: "test-pod-host-name",
},
{
title: "pod's FQDN should be created if enabled",
servicePerPodFqdn: ptr.To(true),
podName: "test-pod-name",
hostName: "",
expectedFqdnPrefix: "test-pod-name",
},
{
title: "pod's FQDN should not be created if hostname is specified and disabled",
servicePerPodFqdn: ptr.To(false),
podName: "test-pod-name",
hostName: "test-pod-host-name",
expectedFqdnPrefix: "",
},
{
title: "pod's FQDN should not be created if disabled",
servicePerPodFqdn: ptr.To(false),
podName: "test-pod-name",
hostName: "",
expectedFqdnPrefix: "",
},
{
title: "hostname should be used for pod's FQDN",
servicePerPodFqdn: ptr.To(true),
podName: "test-pod-name",
hostName: "test-pod-host-name",
expectedFqdnPrefix: "test-pod-host-name",
},
} {
t.Run(test.title, func(t *testing.T) {
sc := &serviceSource{servicePerPodFqdn: test.servicePerPodFqdn}
svc := &v1.Service{
ObjectMeta: metav1.ObjectMeta{Name: "test-service", Namespace: "default"},
}

endpointSlice := &discoveryv1.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{Name: "slice1", Namespace: "default"},
AddressType: discoveryv1.AddressTypeIPv4,
Endpoints: []discoveryv1.Endpoint{
{
TargetRef: &v1.ObjectReference{Kind: "Pod", Name: test.podName},
Conditions: discoveryv1.EndpointConditions{Ready: testutils.ToPtr(false)}, // Not ready
Addresses: []string{"10.0.0.1"},
},
},
}
pods := []*v1.Pod{{
ObjectMeta: metav1.ObjectMeta{Name: test.podName},
Status: v1.PodStatus{PodIP: "10.0.0.1"},
Spec: v1.PodSpec{Hostname: test.hostName},
}}
const serviceHostname = "test-service.example.com"
const endpointsType = "IPv4"
const publishPodIPs = false
const publishNotReadyAddresses = true // This should allow not-ready endpoints

result := sc.processHeadlessEndpointsFromSlices(
svc, pods, []*discoveryv1.EndpointSlice{endpointSlice},
serviceHostname, endpointsType, publishPodIPs, publishNotReadyAddresses)
if len(test.expectedFqdnPrefix) > 0 {
expectedPodFqdn := test.expectedFqdnPrefix + "." + serviceHostname
hasPodFqdn := slices.ContainsFunc(slices.Collect(maps.Keys(result)), func(record endpoint.EndpointKey) bool {
return record.DNSName == expectedPodFqdn
})
assert.True(t, hasPodFqdn, "Endpoint with pod's hostname (%s) should be generated but got: %v", expectedPodFqdn, result)
} else {
hasServiceFqdn := slices.ContainsFunc(slices.Collect(maps.Keys(result)), func(record endpoint.EndpointKey) bool {
return record.DNSName == serviceHostname
})
assert.True(t, hasServiceFqdn && len(result) == 1, "Result should include only service endpoint (%s): %v", serviceHostname, result)
}
})
}
}

// Test getTargetsForDomain with empty ep.Addresses
func TestGetTargetsForDomain_EmptyAddresses(t *testing.T) {
sc := &serviceSource{}
Expand Down
17 changes: 16 additions & 1 deletion source/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"os"
"strings"
"sync"
"time"

Expand All @@ -33,6 +34,7 @@ import (
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/utils/ptr"
gateway "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned"

"sigs.k8s.io/external-dns/source/types"
Expand Down Expand Up @@ -101,6 +103,7 @@ type Config struct {
TraefikDisableNew bool
ExcludeUnschedulable bool
ExposeInternalIPv6 bool
ServicePerPodFqdn *bool
}

func NewSourceConfig(cfg *externaldns.Config) *Config {
Expand Down Expand Up @@ -147,6 +150,18 @@ func NewSourceConfig(cfg *externaldns.Config) *Config {
TraefikDisableNew: cfg.TraefikDisableNew,
ExcludeUnschedulable: cfg.ExcludeUnschedulable,
ExposeInternalIPv6: cfg.ExposeInternalIPV6,
ServicePerPodFqdn: toBoolPtr(cfg.ServicePerPodFqdn),
}
}

func toBoolPtr(value string) *bool {
if len(value) == 0 {
return nil
}
if strings.EqualFold(value, "true") {
return ptr.To(true)
} else {
return ptr.To(false)
}
}

Expand Down Expand Up @@ -429,7 +444,7 @@ func buildServiceSource(ctx context.Context, p ClientGenerator, cfg *Config) (So
if err != nil {
return nil, err
}
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)
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)
}

// buildIngressSource creates an Ingress source for exposing Kubernetes ingresses as DNS records.
Expand Down
8 changes: 8 additions & 0 deletions source/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"github.com/cloudfoundry-community/go-cfclient"
openshift "github.com/openshift/client-go/route/clientset/versioned"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
istioclient "istio.io/client-go/pkg/clientset/versioned"
Expand All @@ -34,6 +35,7 @@ import (
fakeDynamic "k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/kubernetes"
fakeKube "k8s.io/client-go/kubernetes/fake"
"k8s.io/utils/ptr"
"sigs.k8s.io/external-dns/source/types"
gateway "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned"
)
Expand Down Expand Up @@ -279,3 +281,9 @@ func TestBuildWithConfig_InvalidSource(t *testing.T) {
t.Errorf("expected ErrSourceNotFound, got: %v", err)
}
}

func TestToBoolPtr(t *testing.T) {
assert.Equal(t, ptr.To(true), toBoolPtr("true"))
assert.Equal(t, ptr.To(false), toBoolPtr("false"))
assert.Nil(t, toBoolPtr(""))
}