mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-05 17:16:59 +02:00
Add support for dns-controller compat mode for services
This commit is contained in:
parent
90d6eff38e
commit
7a16ab46fa
@ -356,7 +356,7 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("combine-fqdn-annotation", "Combine FQDN template and Annotations instead of overwriting").BoolVar(&cfg.CombineFQDNAndAnnotation)
|
||||
app.Flag("ignore-hostname-annotation", "Ignore hostname annotation when generating DNS names, valid only when using fqdn-template is set (optional, default: false)").BoolVar(&cfg.IgnoreHostnameAnnotation)
|
||||
app.Flag("ignore-ingress-tls-spec", "Ignore tls spec section in ingresses resources, applicable only for ingress sources (optional, default: false)").BoolVar(&cfg.IgnoreIngressTLSSpec)
|
||||
app.Flag("compatibility", "Process annotation semantics from legacy implementations (optional, options: mate, molecule)").Default(defaultConfig.Compatibility).EnumVar(&cfg.Compatibility, "", "mate", "molecule")
|
||||
app.Flag("compatibility", "Process annotation semantics from legacy implementations (optional, options: mate, molecule)").Default(defaultConfig.Compatibility).EnumVar(&cfg.Compatibility, "", "mate", "molecule", "dns-controller")
|
||||
app.Flag("publish-internal-services", "Allow external-dns to publish DNS records for ClusterIP services (optional)").BoolVar(&cfg.PublishInternal)
|
||||
app.Flag("publish-host-ip", "Allow external-dns to publish host-ip for headless services (optional)").BoolVar(&cfg.PublishHostIP)
|
||||
app.Flag("always-publish-not-ready-addresses", "Always publish also not ready addresses for headless services (optional)").BoolVar(&cfg.AlwaysPublishNotReadyAddresses)
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"strings"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
)
|
||||
@ -27,19 +28,25 @@ import (
|
||||
const (
|
||||
mateAnnotationKey = "zalando.org/dnsname"
|
||||
moleculeAnnotationKey = "domainName"
|
||||
// dnsControllerHostnameAnnotationKey is the annotation used for defining the desired hostname when DNS controller compatibility mode
|
||||
dnsControllerHostnameAnnotationKey = "dns.alpha.kubernetes.io/external"
|
||||
// dnsControllerInternalHostnameAnnotationKey is the annotation used for defining the desired hostname when DNS controller compatibility mode
|
||||
dnsControllerInternalHostnameAnnotationKey = "dns.alpha.kubernetes.io/internal"
|
||||
)
|
||||
|
||||
// legacyEndpointsFromService tries to retrieve Endpoints from Services
|
||||
// annotated with legacy annotations.
|
||||
func legacyEndpointsFromService(svc *v1.Service, compatibility string) []*endpoint.Endpoint {
|
||||
switch compatibility {
|
||||
func legacyEndpointsFromService(svc *v1.Service, sc *serviceSource) ([]*endpoint.Endpoint, error) {
|
||||
switch sc.compatibility {
|
||||
case "mate":
|
||||
return legacyEndpointsFromMateService(svc)
|
||||
return legacyEndpointsFromMateService(svc), nil
|
||||
case "molecule":
|
||||
return legacyEndpointsFromMoleculeService(svc)
|
||||
return legacyEndpointsFromMoleculeService(svc), nil
|
||||
case "dns-controller":
|
||||
return legacyEndpointsFromDNSControllerService(svc, sc)
|
||||
}
|
||||
|
||||
return []*endpoint.Endpoint{}
|
||||
return []*endpoint.Endpoint{}, nil
|
||||
}
|
||||
|
||||
// legacyEndpointsFromMateService tries to retrieve Endpoints from Services
|
||||
@ -98,3 +105,102 @@ func legacyEndpointsFromMoleculeService(svc *v1.Service) []*endpoint.Endpoint {
|
||||
|
||||
return endpoints
|
||||
}
|
||||
|
||||
// legacyEndpointsFromDNSControllerService tries to retrieve Endpoints from Services
|
||||
// annotated with DNS Controller's annotation semantics*.
|
||||
func legacyEndpointsFromDNSControllerService(svc *v1.Service, sc *serviceSource) ([]*endpoint.Endpoint, error) {
|
||||
switch svc.Spec.Type {
|
||||
case v1.ServiceTypeNodePort:
|
||||
return legacyEndpointsFromDNSControllerNodePortService(svc, sc)
|
||||
case v1.ServiceTypeLoadBalancer:
|
||||
return legacyEndpointsFromDNSControllerLoadBalancerService(svc), nil
|
||||
}
|
||||
|
||||
return []*endpoint.Endpoint{}, nil
|
||||
}
|
||||
|
||||
// legacyEndpointsFromDNSControllerNodePortService implements DNS controller's semantics for NodePort services.
|
||||
// It will use node role label to check if the node has the "node" role. This means control plane nodes and other
|
||||
// roles will not be used as targets.
|
||||
func legacyEndpointsFromDNSControllerNodePortService(svc *v1.Service, sc *serviceSource) ([]*endpoint.Endpoint, error) {
|
||||
var endpoints []*endpoint.Endpoint
|
||||
|
||||
// Get the desired hostname of the service from the annotations.
|
||||
hostnameAnnotation, isExternal := svc.Annotations[dnsControllerHostnameAnnotationKey]
|
||||
internalHostnameAnnotation, isInternal := svc.Annotations[dnsControllerInternalHostnameAnnotationKey]
|
||||
|
||||
if !isExternal && !isInternal {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// if both annotations are set, we just return empty, mimicking what dns-controller does
|
||||
if isInternal && isExternal {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
nodes, err := sc.nodeInformer.Lister().List(labels.Everything())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var hostnameList []string
|
||||
if isExternal {
|
||||
hostnameList = strings.Split(strings.Replace(hostnameAnnotation, " ", "", -1), ",")
|
||||
} else {
|
||||
hostnameList = strings.Split(strings.Replace(internalHostnameAnnotation, " ", "", -1), ",")
|
||||
}
|
||||
|
||||
for _, hostname := range hostnameList {
|
||||
for _, node := range nodes {
|
||||
_, isNode := node.Labels["node-role.kubernetes.io/node"]
|
||||
if !isNode {
|
||||
continue
|
||||
}
|
||||
for _, address := range node.Status.Addresses {
|
||||
if address.Type == v1.NodeExternalIP && isExternal {
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, endpoint.RecordTypeA, address.Address))
|
||||
}
|
||||
if address.Type == v1.NodeInternalIP && isInternal {
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, endpoint.RecordTypeA, address.Address))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// legacyEndpointsFromDNSControllerLoadBalancerService will respect both annotations, but
|
||||
// will not care if the load balancer actually is internal or not.
|
||||
func legacyEndpointsFromDNSControllerLoadBalancerService(svc *v1.Service) []*endpoint.Endpoint {
|
||||
var endpoints []*endpoint.Endpoint
|
||||
|
||||
// Get the desired hostname of the service from the annotations.
|
||||
hostnameAnnotation, hasExternal := svc.Annotations[dnsControllerHostnameAnnotationKey]
|
||||
internalHostnameAnnotation, hasInternal := svc.Annotations[dnsControllerInternalHostnameAnnotationKey]
|
||||
|
||||
if !hasExternal && !hasInternal {
|
||||
return nil
|
||||
}
|
||||
|
||||
var hostnameList []string
|
||||
if hasExternal {
|
||||
hostnameList = append(hostnameList, strings.Split(strings.Replace(hostnameAnnotation, " ", "", -1), ",")...)
|
||||
}
|
||||
if hasInternal {
|
||||
hostnameList = append(hostnameList, strings.Split(strings.Replace(internalHostnameAnnotation, " ", "", -1), ",")...)
|
||||
}
|
||||
|
||||
for _, hostname := range hostnameList {
|
||||
// Create a corresponding endpoint for each configured external entrypoint.
|
||||
for _, lb := range svc.Status.LoadBalancer.Ingress {
|
||||
if lb.IP != "" {
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, endpoint.RecordTypeA, lb.IP))
|
||||
}
|
||||
if lb.Hostname != "" {
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint(hostname, endpoint.RecordTypeCNAME, lb.Hostname))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints
|
||||
}
|
||||
|
@ -187,7 +187,10 @@ func (sc *serviceSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e
|
||||
|
||||
// process legacy annotations if no endpoints were returned and compatibility mode is enabled.
|
||||
if len(svcEndpoints) == 0 && sc.compatibility != "" {
|
||||
svcEndpoints = legacyEndpointsFromService(svc, sc.compatibility)
|
||||
svcEndpoints, err = legacyEndpointsFromService(svc, sc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// apply template if none of the above is found
|
||||
|
@ -868,6 +868,59 @@ func testServiceSourceEndpoints(t *testing.T) {
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"load balancer services annotated with DNS Controller annotations return an endpoint with A and CNAME targets in compatibility mode",
|
||||
"",
|
||||
"",
|
||||
"testing",
|
||||
"foo",
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"dns-controller",
|
||||
"",
|
||||
false,
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
dnsControllerInternalHostnameAnnotationKey: "internal.foo.example.org",
|
||||
},
|
||||
"",
|
||||
[]string{},
|
||||
[]string{"1.2.3.4", "lb.example.com"},
|
||||
[]string{},
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "internal.foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
|
||||
{DNSName: "internal.foo.example.org", Targets: endpoint.Targets{"lb.example.com"}},
|
||||
},
|
||||
false,
|
||||
}, {
|
||||
"load balancer services annotated with DNS Controller annotations return an endpoint with both annotations in compatibility mode",
|
||||
"",
|
||||
"",
|
||||
"testing",
|
||||
"foo",
|
||||
v1.ServiceTypeLoadBalancer,
|
||||
"dns-controller",
|
||||
"",
|
||||
false,
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
dnsControllerInternalHostnameAnnotationKey: "internal.foo.example.org., internal.bar.example.org",
|
||||
dnsControllerHostnameAnnotationKey: "foo.example.org., bar.example.org",
|
||||
},
|
||||
"",
|
||||
[]string{},
|
||||
[]string{"1.2.3.4"},
|
||||
[]string{},
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
|
||||
{DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
|
||||
{DNSName: "internal.foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
|
||||
{DNSName: "internal.bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}},
|
||||
},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"not annotated services with set fqdnTemplate return an endpoint with target IP",
|
||||
"",
|
||||
@ -1603,7 +1656,7 @@ func TestClusterIpServices(t *testing.T) {
|
||||
}
|
||||
|
||||
// testNodePortServices tests that various services generate the correct endpoints.
|
||||
func TestNodePortServices(t *testing.T) {
|
||||
func TestServiceSourceNodePortServices(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
title string
|
||||
targetNamespace string
|
||||
@ -1988,6 +2041,212 @@ func TestNodePortServices(t *testing.T) {
|
||||
[]int{},
|
||||
[]v1.PodPhase{},
|
||||
},
|
||||
{
|
||||
"node port services annotated DNS Controller annotations return an endpoint where all targets has the node role",
|
||||
"",
|
||||
"",
|
||||
"testing",
|
||||
"foo",
|
||||
v1.ServiceTypeNodePort,
|
||||
v1.ServiceExternalTrafficPolicyTypeCluster,
|
||||
"dns-controller",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
dnsControllerInternalHostnameAnnotationKey: "internal.foo.example.org., internal.bar.example.org",
|
||||
},
|
||||
nil,
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "internal.foo.example.org", Targets: endpoint.Targets{"10.0.1.1"}},
|
||||
{DNSName: "internal.bar.example.org", Targets: endpoint.Targets{"10.0.1.1"}},
|
||||
},
|
||||
false,
|
||||
[]*v1.Node{{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node1",
|
||||
Labels: map[string]string{
|
||||
"node-role.kubernetes.io/node": "",
|
||||
},
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Addresses: []v1.NodeAddress{
|
||||
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
|
||||
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node2",
|
||||
Labels: map[string]string{
|
||||
"node-role.kubernetes.io/control-plane": "",
|
||||
},
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Addresses: []v1.NodeAddress{
|
||||
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
|
||||
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
[]string{},
|
||||
[]int{},
|
||||
[]v1.PodPhase{},
|
||||
},
|
||||
{
|
||||
"node port services annotated with internal DNS Controller annotations return an endpoint in compatibility mode",
|
||||
"",
|
||||
"",
|
||||
"testing",
|
||||
"foo",
|
||||
v1.ServiceTypeNodePort,
|
||||
v1.ServiceExternalTrafficPolicyTypeCluster,
|
||||
"dns-controller",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
dnsControllerInternalHostnameAnnotationKey: "internal.foo.example.org., internal.bar.example.org",
|
||||
},
|
||||
nil,
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "internal.foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}},
|
||||
{DNSName: "internal.bar.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}},
|
||||
},
|
||||
false,
|
||||
[]*v1.Node{{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node1",
|
||||
Labels: map[string]string{
|
||||
"node-role.kubernetes.io/node": "",
|
||||
},
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Addresses: []v1.NodeAddress{
|
||||
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
|
||||
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node2",
|
||||
Labels: map[string]string{
|
||||
"node-role.kubernetes.io/node": "",
|
||||
},
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Addresses: []v1.NodeAddress{
|
||||
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
|
||||
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
[]string{},
|
||||
[]int{},
|
||||
[]v1.PodPhase{},
|
||||
},
|
||||
{
|
||||
"node port services annotated with external DNS Controller annotations return an endpoint in compatibility mode",
|
||||
"",
|
||||
"",
|
||||
"testing",
|
||||
"foo",
|
||||
v1.ServiceTypeNodePort,
|
||||
v1.ServiceExternalTrafficPolicyTypeCluster,
|
||||
"dns-controller",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
dnsControllerHostnameAnnotationKey: "foo.example.org., bar.example.org",
|
||||
},
|
||||
nil,
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}},
|
||||
{DNSName: "bar.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}},
|
||||
},
|
||||
false,
|
||||
[]*v1.Node{{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node1",
|
||||
Labels: map[string]string{
|
||||
"node-role.kubernetes.io/node": "",
|
||||
},
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Addresses: []v1.NodeAddress{
|
||||
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
|
||||
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node2",
|
||||
Labels: map[string]string{
|
||||
"node-role.kubernetes.io/node": "",
|
||||
},
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Addresses: []v1.NodeAddress{
|
||||
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
|
||||
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
[]string{},
|
||||
[]int{},
|
||||
[]v1.PodPhase{},
|
||||
},
|
||||
{
|
||||
"node port services annotated with both dns-controller annotations return an empty set of addons",
|
||||
"",
|
||||
"",
|
||||
"testing",
|
||||
"foo",
|
||||
v1.ServiceTypeNodePort,
|
||||
v1.ServiceExternalTrafficPolicyTypeCluster,
|
||||
"dns-controller",
|
||||
"",
|
||||
false,
|
||||
map[string]string{},
|
||||
map[string]string{
|
||||
dnsControllerInternalHostnameAnnotationKey: "internal.foo.example.org., internal.bar.example.org",
|
||||
dnsControllerHostnameAnnotationKey: "foo.example.org., bar.example.org",
|
||||
},
|
||||
nil,
|
||||
[]*endpoint.Endpoint{},
|
||||
false,
|
||||
[]*v1.Node{{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node1",
|
||||
Labels: map[string]string{
|
||||
"node-role.kubernetes.io/node": "",
|
||||
},
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Addresses: []v1.NodeAddress{
|
||||
{Type: v1.NodeExternalIP, Address: "54.10.11.1"},
|
||||
{Type: v1.NodeInternalIP, Address: "10.0.1.1"},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "node2",
|
||||
Labels: map[string]string{
|
||||
"node-role.kubernetes.io/node": "",
|
||||
},
|
||||
},
|
||||
Status: v1.NodeStatus{
|
||||
Addresses: []v1.NodeAddress{
|
||||
{Type: v1.NodeExternalIP, Address: "54.10.11.2"},
|
||||
{Type: v1.NodeInternalIP, Address: "10.0.1.2"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
[]string{},
|
||||
[]int{},
|
||||
[]v1.PodPhase{},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.title, func(t *testing.T) {
|
||||
// Create a Kubernetes testing client
|
||||
|
Loading…
Reference in New Issue
Block a user