Implement target annotation for more sources

This commit is contained in:
John Gardiner Myers 2023-09-19 14:50:14 -07:00
parent 859892fc72
commit 4a32aaec01
12 changed files with 388 additions and 78 deletions

View File

@ -5,21 +5,21 @@ ExternalDNS sources support a number of annotations on the Kubernetes resources
The following table documents which sources support which annotations:
| Source | controller | hostname | internal-hostname | target | ttl | (provider-specific) |
|--------------|------------|----------|-------------------|---------|-----|---------------------|
| Ambassador | | | | | Yes | |
|--------------|------------|----------|-------------------|---------|---------|---------------------|
| Ambassador | | | | Yes | Yes | |
| Connector | | | | | | |
| Contour | Yes | Yes[^1] | | Yes | Yes | Yes |
| CloudFoundry | | | | | | |
| CRD | | | | | | |
| F5 | | | | | Yes | |
| F5 | | | | Yes | Yes | |
| Gateway | Yes | Yes[^1] | | Yes[^4] | Yes | Yes |
| Gloo | | | | | Yes | Yes |
| Gloo | | | | Yes | Yes[^5] | Yes[^5] |
| Ingress | Yes | Yes[^1] | | Yes | Yes | Yes |
| Istio | Yes | Yes[^1] | | Yes | Yes | Yes |
| Kong | | Yes | | | Yes | Yes |
| Node | Yes | | | | Yes | |
| Kong | | Yes | | Yes | Yes | Yes |
| Node | Yes | | | Yes | Yes | |
| OpenShift | Yes | Yes[^1] | | Yes | Yes | Yes |
| Pod | | Yes | Yes | | | |
| Pod | | Yes | Yes | Yes | | |
| Service | Yes | Yes[^1] | Yes[^1][^2] | Yes[^3] | Yes | Yes |
| Skipper | Yes | Yes[^1] | | Yes | Yes | Yes |
| Traefik | | Yes | | Yes | Yes | Yes |
@ -27,7 +27,8 @@ The following table documents which sources support which annotations:
[^1]: Unless the `--ignore-hostname-annotation` flag is specified.
[^2]: Only behaves differently than `hostname` for `Service`s of type `ClusterIP` or `LoadBalancer`.
[^3]: Also supported on `Pods` referenced from a headless `Service`'s `Endpoints`.
[^4]: The annotation should be on the `Gateway`
[^4]: The annotation must be on the `Gateway`.
[^5]: The annotation must be on the listener's `VirtualService`.
## external-dns.alpha.kubernetes.io/access

View File

@ -133,11 +133,14 @@ func (sc *ambassadorHostSource) Endpoints(ctx context.Context) ([]*endpoint.Endp
continue
}
targets, err := sc.targetsFromAmbassadorLoadBalancer(ctx, service)
targets := getTargetsFromTargetAnnotation(host.Annotations)
if len(targets) == 0 {
targets, err = sc.targetsFromAmbassadorLoadBalancer(ctx, service)
if err != nil {
log.Warningf("Could not find targets for service %s for Host %s: %v", service, fullname, err)
continue
}
}
hostEndpoints, err := sc.endpointsFromHost(ctx, host, targets)
if err != nil {

View File

@ -152,37 +152,16 @@ func (vs *f5VirtualServerSource) endpointsFromVirtualServers(virtualServers []*f
return nil, err
}
if virtualServer.Spec.VirtualServerAddress != "" {
ep := &endpoint.Endpoint{
Targets: endpoint.Targets{
virtualServer.Spec.VirtualServerAddress,
},
RecordType: "A",
DNSName: virtualServer.Spec.Host,
Labels: endpoint.NewLabels(),
RecordTTL: ttl,
targets := getTargetsFromTargetAnnotation(virtualServer.Annotations)
if len(targets) == 0 && virtualServer.Spec.VirtualServerAddress != "" {
targets = append(targets, virtualServer.Spec.VirtualServerAddress)
}
if len(targets) == 0 && virtualServer.Status.VSAddress != "" {
targets = append(targets, virtualServer.Status.VSAddress)
}
vs.setResourceLabel(virtualServer, ep)
endpoints = append(endpoints, ep)
continue
}
if virtualServer.Status.VSAddress != "" {
ep := &endpoint.Endpoint{
Targets: endpoint.Targets{
virtualServer.Status.VSAddress,
},
RecordType: "A",
DNSName: virtualServer.Spec.Host,
Labels: endpoint.NewLabels(),
RecordTTL: ttl,
}
vs.setResourceLabel(virtualServer, ep)
endpoints = append(endpoints, ep)
continue
}
resource := fmt.Sprintf("f5-virtualserver/%s/%s", virtualServer.Namespace, virtualServer.Name)
endpoints = append(endpoints, endpointsForHostname(virtualServer.Spec.Host, targets, ttl, nil, "", resource)...)
}
return endpoints, nil
@ -234,7 +213,3 @@ func (vs *f5VirtualServerSource) filterByAnnotations(virtualServers []*f5.Virtua
return filteredList, nil
}
func (vs *f5VirtualServerSource) setResourceLabel(virtualServer *f5.VirtualServer, ep *endpoint.Endpoint) {
ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("f5-virtualserver/%s/%s", virtualServer.Namespace, virtualServer.Name)
}

View File

@ -44,6 +44,41 @@ func TestF5VirtualServerEndpoints(t *testing.T) {
virtualServer f5.VirtualServer
expected []*endpoint.Endpoint
}{
{
name: "F5 VirtualServer with target annotation",
annotationFilter: "",
virtualServer: f5.VirtualServer{
TypeMeta: metav1.TypeMeta{
APIVersion: f5VirtualServerGVR.GroupVersion().String(),
Kind: "VirtualServer",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-vs",
Namespace: defaultF5VirtualServerNamespace,
Annotations: map[string]string{
targetAnnotationKey: "192.168.1.150",
},
},
Spec: f5.VirtualServerSpec{
Host: "www.example.com",
VirtualServerAddress: "192.168.1.100",
},
Status: f5.VirtualServerStatus{
VSAddress: "192.168.1.200",
},
},
expected: []*endpoint.Endpoint{
{
DNSName: "www.example.com",
Targets: []string{"192.168.1.150"},
RecordType: endpoint.RecordTypeA,
RecordTTL: 0,
Labels: endpoint.Labels{
"resource": "f5-virtualserver/virtualserver/test-vs",
},
},
},
},
{
name: "F5 VirtualServer with host and virtualServerAddress set",
annotationFilter: "",
@ -60,6 +95,9 @@ func TestF5VirtualServerEndpoints(t *testing.T) {
Host: "www.example.com",
VirtualServerAddress: "192.168.1.100",
},
Status: f5.VirtualServerStatus{
VSAddress: "192.168.1.200",
},
},
expected: []*endpoint.Endpoint{
{

View File

@ -132,11 +132,16 @@ func (gs *glooSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
return nil, err
}
log.Debugf("Gloo: Find %s proxy", proxy.Metadata.Name)
proxyTargets, err := gs.proxyTargets(ctx, proxy.Metadata.Name, ns)
proxyTargets := getTargetsFromTargetAnnotation(proxy.Metadata.Annotations)
if len(proxyTargets) == 0 {
proxyTargets, err = gs.proxyTargets(ctx, proxy.Metadata.Name, ns)
if err != nil {
return nil, err
}
}
log.Debugf("Gloo[%s]: Find %d target(s) (%+v)", proxy.Metadata.Name, len(proxyTargets), proxyTargets)
proxyEndpoints, err := gs.generateEndpointsFromProxy(ctx, &proxy, proxyTargets)
if err != nil {
return nil, err

View File

@ -302,6 +302,96 @@ var proxyMetadataStaticSource = metav1.PartialObjectMetadata{
},
}
// Proxy with target annotation test
var targetAnnotatedProxy = proxy{
TypeMeta: metav1.TypeMeta{
APIVersion: proxyGVR.GroupVersion().String(),
Kind: "Proxy",
},
Metadata: metav1.ObjectMeta{
Name: "target-ann",
Namespace: defaultGlooNamespace,
Annotations: map[string]string{
"external-dns.alpha.kubernetes.io/target": "203.2.45.7",
},
},
Spec: proxySpec{
Listeners: []proxySpecListener{
{
HTTPListener: proxySpecHTTPListener{
VirtualHosts: []proxyVirtualHost{
{
Domains: []string{"i.test"},
Metadata: proxyVirtualHostMetadata{
Source: []proxyVirtualHostMetadataSource{
{
Kind: "*v1.Unknown",
Name: "my-unknown-svc",
Namespace: "unknown",
},
},
},
},
{
Domains: []string{"j.test"},
Metadata: proxyVirtualHostMetadata{
Source: []proxyVirtualHostMetadataSource{
{
Kind: "*v1.VirtualService",
Name: "my-annotated-svc",
Namespace: "internal",
},
},
},
},
},
},
},
},
},
}
var targetAnnotatedProxySvc = corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: targetAnnotatedProxy.Metadata.Name,
Namespace: targetAnnotatedProxy.Metadata.Namespace,
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeLoadBalancer,
},
Status: corev1.ServiceStatus{
LoadBalancer: corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{
{
IP: "203.1.115.1",
},
{
IP: "203.1.115.2",
},
{
IP: "203.1.115.3",
},
},
},
},
}
var targetAnnotatedProxySource = metav1.PartialObjectMetadata{
TypeMeta: metav1.TypeMeta{
APIVersion: virtualServiceGVR.GroupVersion().String(),
Kind: "VirtualService",
},
ObjectMeta: metav1.ObjectMeta{
Name: targetAnnotatedProxy.Spec.Listeners[0].HTTPListener.VirtualHosts[1].Metadata.Source[0].Name,
Namespace: targetAnnotatedProxy.Spec.Listeners[0].HTTPListener.VirtualHosts[1].Metadata.Source[0].Namespace,
Annotations: map[string]string{
"external-dns.alpha.kubernetes.io/ttl": "460",
"external-dns.alpha.kubernetes.io/aws-geolocation-country-code": "IT",
"external-dns.alpha.kubernetes.io/set-identifier": "identifier-annotated",
},
},
}
func TestGlooSource(t *testing.T) {
t.Parallel()
@ -318,10 +408,12 @@ func TestGlooSource(t *testing.T) {
internalProxyUnstructured := unstructured.Unstructured{}
externalProxyUnstructured := unstructured.Unstructured{}
proxyMetadataStaticUnstructured := unstructured.Unstructured{}
targetAnnotatedProxyUnstructured := unstructured.Unstructured{}
internalProxySourceUnstructured := unstructured.Unstructured{}
externalProxySourceUnstructured := unstructured.Unstructured{}
proxyMetadataStaticSourceUnstructured := unstructured.Unstructured{}
targetAnnotatedProxySourceUnstructured := unstructured.Unstructured{}
internalProxyAsJSON, err := json.Marshal(internalProxy)
assert.NoError(t, err)
@ -332,6 +424,9 @@ func TestGlooSource(t *testing.T) {
proxyMetadataStaticAsJSON, err := json.Marshal(proxyMetadataStatic)
assert.NoError(t, err)
targetAnnotatedProxyAsJSON, err := json.Marshal(targetAnnotatedProxy)
assert.NoError(t, err)
internalProxySvcAsJSON, err := json.Marshal(internalProxySource)
assert.NoError(t, err)
@ -341,13 +436,18 @@ func TestGlooSource(t *testing.T) {
proxyMetadataStaticSvcAsJSON, err := json.Marshal(proxyMetadataStaticSource)
assert.NoError(t, err)
targetAnnotatedProxySvcAsJSON, err := json.Marshal(targetAnnotatedProxySource)
assert.NoError(t, err)
assert.NoError(t, internalProxyUnstructured.UnmarshalJSON(internalProxyAsJSON))
assert.NoError(t, externalProxyUnstructured.UnmarshalJSON(externalProxyAsJSON))
assert.NoError(t, proxyMetadataStaticUnstructured.UnmarshalJSON(proxyMetadataStaticAsJSON))
assert.NoError(t, targetAnnotatedProxyUnstructured.UnmarshalJSON(targetAnnotatedProxyAsJSON))
assert.NoError(t, internalProxySourceUnstructured.UnmarshalJSON(internalProxySvcAsJSON))
assert.NoError(t, externalProxySourceUnstructured.UnmarshalJSON(externalProxySvcAsJSON))
assert.NoError(t, proxyMetadataStaticSourceUnstructured.UnmarshalJSON(proxyMetadataStaticSvcAsJSON))
assert.NoError(t, targetAnnotatedProxySourceUnstructured.UnmarshalJSON(targetAnnotatedProxySvcAsJSON))
// Create proxy resources
_, err = fakeDynamicClient.Resource(proxyGVR).Namespace(defaultGlooNamespace).Create(context.Background(), &internalProxyUnstructured, metav1.CreateOptions{})
@ -356,6 +456,8 @@ func TestGlooSource(t *testing.T) {
assert.NoError(t, err)
_, err = fakeDynamicClient.Resource(proxyGVR).Namespace(defaultGlooNamespace).Create(context.Background(), &proxyMetadataStaticUnstructured, metav1.CreateOptions{})
assert.NoError(t, err)
_, err = fakeDynamicClient.Resource(proxyGVR).Namespace(defaultGlooNamespace).Create(context.Background(), &targetAnnotatedProxyUnstructured, metav1.CreateOptions{})
assert.NoError(t, err)
// Create proxy source
_, err = fakeDynamicClient.Resource(virtualServiceGVR).Namespace(internalProxySource.Namespace).Create(context.Background(), &internalProxySourceUnstructured, metav1.CreateOptions{})
@ -364,6 +466,8 @@ func TestGlooSource(t *testing.T) {
assert.NoError(t, err)
_, err = fakeDynamicClient.Resource(virtualServiceGVR).Namespace(proxyMetadataStaticSource.Namespace).Create(context.Background(), &proxyMetadataStaticSourceUnstructured, metav1.CreateOptions{})
assert.NoError(t, err)
_, err = fakeDynamicClient.Resource(virtualServiceGVR).Namespace(targetAnnotatedProxySource.Namespace).Create(context.Background(), &targetAnnotatedProxySourceUnstructured, metav1.CreateOptions{})
assert.NoError(t, err)
// Create proxy service resources
_, err = fakeKubernetesClient.CoreV1().Services(internalProxySvc.GetNamespace()).Create(context.Background(), &internalProxySvc, metav1.CreateOptions{})
@ -372,10 +476,12 @@ func TestGlooSource(t *testing.T) {
assert.NoError(t, err)
_, err = fakeKubernetesClient.CoreV1().Services(proxyMetadataStaticSvc.GetNamespace()).Create(context.Background(), &proxyMetadataStaticSvc, metav1.CreateOptions{})
assert.NoError(t, err)
_, err = fakeKubernetesClient.CoreV1().Services(targetAnnotatedProxySvc.GetNamespace()).Create(context.Background(), &targetAnnotatedProxySvc, metav1.CreateOptions{})
assert.NoError(t, err)
endpoints, err := source.Endpoints(context.Background())
assert.NoError(t, err)
assert.Len(t, endpoints, 8)
assert.Len(t, endpoints, 10)
assert.ElementsMatch(t, endpoints, []*endpoint.Endpoint{
{
DNSName: "a.test",
@ -459,5 +565,26 @@ func TestGlooSource(t *testing.T) {
},
},
},
{
DNSName: "i.test",
Targets: []string{"203.2.45.7"},
RecordType: endpoint.RecordTypeA,
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{},
},
{
DNSName: "j.test",
Targets: []string{"203.2.45.7"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "identifier-annotated",
RecordTTL: 460,
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{
endpoint.ProviderSpecificProperty{
Name: "aws/geolocation-country-code",
Value: "IT",
},
},
},
})
}

View File

@ -124,7 +124,8 @@ func (sc *kongTCPIngressSource) Endpoints(ctx context.Context) ([]*endpoint.Endp
var endpoints []*endpoint.Endpoint
for _, tcpIngress := range tcpIngresses {
var targets endpoint.Targets
targets := getTargetsFromTargetAnnotation(tcpIngress.Annotations)
if len(targets) == 0 {
for _, lb := range tcpIngress.Status.LoadBalancer.Ingress {
if lb.IP != "" {
targets = append(targets, lb.IP)
@ -133,6 +134,7 @@ func (sc *kongTCPIngressSource) Endpoints(ctx context.Context) ([]*endpoint.Endp
targets = append(targets, lb.Hostname)
}
}
}
fullname := fmt.Sprintf("%s/%s", tcpIngress.Namespace, tcpIngress.Name)

View File

@ -220,6 +220,65 @@ func TestKongTCPIngressEndpoints(t *testing.T) {
},
},
},
{
title: "TCPIngress with target annotation",
tcpProxy: TCPIngress{
TypeMeta: metav1.TypeMeta{
APIVersion: kongGroupdVersionResource.GroupVersion().String(),
Kind: "TCPIngress",
},
ObjectMeta: metav1.ObjectMeta{
Name: "tcp-ingress-sni",
Namespace: defaultKongNamespace,
Annotations: map[string]string{
"kubernetes.io/ingress.class": "kong",
"external-dns.alpha.kubernetes.io/target": "203.2.45.7",
},
},
Spec: tcpIngressSpec{
Rules: []tcpIngressRule{
{
Port: 30002,
Host: "b.example.com",
},
{
Port: 30003,
Host: "c.example.com",
},
},
},
Status: tcpIngressStatus{
LoadBalancer: corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{
{
Hostname: "a123456769a314e71861a4303f06a3bd-1291189659.us-east-1.elb.amazonaws.com",
},
},
},
},
},
expected: []*endpoint.Endpoint{
{
DNSName: "b.example.com",
Targets: []string{"203.2.45.7"},
RecordType: endpoint.RecordTypeA,
RecordTTL: 0,
Labels: endpoint.Labels{
"resource": "tcpingress/kong/tcp-ingress-sni",
},
ProviderSpecific: endpoint.ProviderSpecific{},
},
{
DNSName: "c.example.com",
Targets: []string{"203.2.45.7"},
RecordType: endpoint.RecordTypeA,
Labels: endpoint.Labels{
"resource": "tcpingress/kong/tcp-ingress-sni",
},
ProviderSpecific: endpoint.ProviderSpecific{},
},
},
},
} {
ti := ti
t.Run(ti.title, func(t *testing.T) {

View File

@ -130,10 +130,13 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
log.Debugf("not applying template for %s", node.Name)
}
addrs, err := ns.nodeAddresses(node)
addrs := getTargetsFromTargetAnnotation(node.Annotations)
if len(addrs) == 0 {
addrs, err = ns.nodeAddresses(node)
if err != nil {
return nil, fmt.Errorf("failed to get node address from %s: %w", node.Name, err)
}
}
ep.Labels = endpoint.NewLabels()
for _, addr := range addrs {

View File

@ -205,6 +205,17 @@ func testNodeSourceEndpoints(t *testing.T) {
nodeAddresses: []v1.NodeAddress{},
expectError: true,
},
{
title: "node with target annotation",
nodeName: "node1.example.org",
nodeAddresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "1.2.3.4"}},
annotations: map[string]string{
"external-dns.alpha.kubernetes.io/target": "203.2.45.7",
},
expected: []*endpoint.Endpoint{
{RecordType: "A", DNSName: "node1.example.org", Targets: endpoint.Targets{"203.2.45.7"}},
},
},
{
title: "annotated node without annotation filter returns endpoint",
nodeName: "node1",

View File

@ -89,16 +89,25 @@ func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
continue
}
targets := getTargetsFromTargetAnnotation(pod.Annotations)
if domainAnnotation, ok := pod.Annotations[internalHostnameAnnotationKey]; ok {
domainList := splitHostnameAnnotation(domainAnnotation)
for _, domain := range domainList {
if len(targets) == 0 {
addToEndpointMap(endpointMap, domain, suitableType(pod.Status.PodIP), pod.Status.PodIP)
} else {
for _, target := range targets {
addToEndpointMap(endpointMap, domain, suitableType(target), target)
}
}
}
}
if domainAnnotation, ok := pod.Annotations[hostnameAnnotationKey]; ok {
domainList := splitHostnameAnnotation(domainAnnotation)
for _, domain := range domainList {
if len(targets) == 0 {
node, _ := ps.nodeInformer.Lister().Get(pod.Spec.NodeName)
for _, address := range node.Status.Addresses {
recordType := suitableType(address.Address)
@ -107,6 +116,11 @@ func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
addToEndpointMap(endpointMap, domain, recordType, address.Address)
}
}
} else {
for _, target := range targets {
addToEndpointMap(endpointMap, domain, suitableType(target), target)
}
}
}
}

View File

@ -316,6 +316,78 @@ func TestPodSource(t *testing.T) {
},
},
},
{
"create records based on pod's target annotation",
"",
"",
[]*endpoint.Endpoint{
{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"208.1.2.1", "208.1.2.2"}, RecordType: endpoint.RecordTypeA},
{DNSName: "internal.a.foo.example.org", Targets: endpoint.Targets{"208.1.2.1", "208.1.2.2"}, RecordType: endpoint.RecordTypeA},
},
false,
[]*corev1.Node{
{
ObjectMeta: metav1.ObjectMeta{
Name: "my-node1",
},
Status: corev1.NodeStatus{
Addresses: []corev1.NodeAddress{
{Type: corev1.NodeExternalIP, Address: "54.10.11.1"},
{Type: corev1.NodeInternalIP, Address: "10.0.1.1"},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "my-node2",
},
Status: corev1.NodeStatus{
Addresses: []corev1.NodeAddress{
{Type: corev1.NodeExternalIP, Address: "54.10.11.2"},
{Type: corev1.NodeInternalIP, Address: "10.0.1.2"},
},
},
},
},
[]*corev1.Pod{
{
ObjectMeta: metav1.ObjectMeta{
Name: "my-pod1",
Namespace: "kube-system",
Annotations: map[string]string{
internalHostnameAnnotationKey: "internal.a.foo.example.org",
hostnameAnnotationKey: "a.foo.example.org",
targetAnnotationKey: "208.1.2.1",
},
},
Spec: corev1.PodSpec{
HostNetwork: true,
NodeName: "my-node1",
},
Status: corev1.PodStatus{
PodIP: "10.0.1.1",
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "my-pod2",
Namespace: "kube-system",
Annotations: map[string]string{
internalHostnameAnnotationKey: "internal.a.foo.example.org",
hostnameAnnotationKey: "a.foo.example.org",
targetAnnotationKey: "208.1.2.2",
},
},
Spec: corev1.PodSpec{
HostNetwork: true,
NodeName: "my-node2",
},
Status: corev1.PodStatus{
PodIP: "10.0.1.2",
},
},
},
},
{
"create multiple records",
"",