Add support for dns-controller compat mode for services

This commit is contained in:
Ole Markus With 2021-03-31 15:45:35 +02:00
parent 90d6eff38e
commit 7a16ab46fa
4 changed files with 376 additions and 8 deletions

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -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