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/annotations/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ Some providers define their own annotations. Cloud-specific annotations have key
| Cloud | Annotation prefix |
|------------|------------------------------------------------|
| AWS | `external-dns.alpha.kubernetes.io/aws-` |
| Azure | `external-dns.alpha.kubernetes.io/azure-` |
| CloudFlare | `external-dns.alpha.kubernetes.io/cloudflare-` |
| Scaleway | `external-dns.alpha.kubernetes.io/scw-` |

Expand Down
56 changes: 56 additions & 0 deletions docs/tutorials/azure.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,62 @@ Ensure that your nginx-ingress deployment has the following arg: added to it:

For more details see here: [nginx-ingress external-dns](https://github.com/kubernetes-sigs/external-dns/blob/HEAD/docs/faq.md#why-is-externaldns-only-adding-a-single-ip-address-in-route-53-on-aws-when-using-the-nginx-ingress-controller-how-do-i-get-it-to-use-the-fqdn-of-the-elb-assigned-to-my-nginx-ingress-controller-service-instead)

## DNS Record Metadata (Tags)

External-DNS supports setting Azure resource metadata (tags) on DNS records using annotations on Kubernetes resources.

### Usage with Ingress

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
annotations:
external-dns.alpha.kubernetes.io/azure-metadata-cost-center: "12345"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not looks consistent with other providers like aws or cloudflare

Could we have

external-dns.alpha.kubernetes.io/azure-tags: "cost-center=12345,owner=backend-team"

Current approach will require way to many annotaions on objects, and I did not get from first look that two annotations actually represent tags. There are of course limitations on number of annotitionas and character limit on annotations values. I think we better be consistent for now.

Kubernetes has 63 characters limitation on the annotation name. Example external-dns.alpha.kubernetes.io/azure-metadata-cost-center is already 59....

external-dns.alpha.kubernetes.io/azure-metadata-owner: backend-team
spec:
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-service
port:
number: 80
```

### Usage with Gateway API (HTTPRoute)

```yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-route
annotations:
external-dns.alpha.kubernetes.io/azure-metadata-environment: production
external-dns.alpha.kubernetes.io/azure-metadata-app: myapp
spec:
parentRefs:
- name: my-gateway
hostnames:
- "api.example.com"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: This is under discussion in gateway-api if this is what we should support or not.

rules:
- backendRefs:
- name: my-service
port: 8080
```

**Note:** Metadata annotations must be placed directly on each route resource (HTTPRoute, TLSRoute, GRPCRoute, etc.). Gateway annotations are not automatically inherited by routes.

### Annotation Format

Metadata annotations must follow the format:
`external-dns.alpha.kubernetes.io/azure-metadata-<key>: <value>`

## Deploy ExternalDNS

Connect your `kubectl` client to the cluster you want to test ExternalDNS with. Then apply one of the following manifests file to deploy ExternalDNS.
Expand Down
43 changes: 43 additions & 0 deletions provider/azure/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import (

const (
defaultTTL = 300
// Azure-specific provider properties
providerSpecificMetadataPrefix = "azure/metadata-"
)

// ZonesClient is an interface of dns.ZoneClient that can be stubbed for testing.
Expand Down Expand Up @@ -145,6 +147,7 @@ func (p *AzureProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, erro
ttl = endpoint.TTL(*recordSet.Properties.TTL)
}
ep := endpoint.NewEndpointWithTTL(name, recordType, ttl, targets...)
extractMetadataFromRecordSet(ep, recordSet)
log.Debugf(
"Found %s record for '%s' with target '%s'.",
ep.RecordType,
Expand Down Expand Up @@ -346,6 +349,8 @@ func (p *AzureProvider) newRecordSet(endpoint *endpoint.Endpoint) (dns.RecordSet
if endpoint.RecordTTL.IsConfigured() {
ttl = int64(endpoint.RecordTTL)
}
// Extract metadata from ProviderSpecific properties
metadata := extractMetadataFromEndpoint(endpoint)
switch dns.RecordType(endpoint.RecordType) {
case dns.RecordTypeA:
aRecords := make([]*dns.ARecord, len(endpoint.Targets))
Expand All @@ -358,6 +363,7 @@ func (p *AzureProvider) newRecordSet(endpoint *endpoint.Endpoint) (dns.RecordSet
Properties: &dns.RecordSetProperties{
TTL: to.Ptr(ttl),
ARecords: aRecords,
Metadata: metadata,
},
}, nil
case dns.RecordTypeAAAA:
Expand All @@ -371,6 +377,7 @@ func (p *AzureProvider) newRecordSet(endpoint *endpoint.Endpoint) (dns.RecordSet
Properties: &dns.RecordSetProperties{
TTL: to.Ptr(ttl),
AaaaRecords: aaaaRecords,
Metadata: metadata,
},
}, nil
case dns.RecordTypeCNAME:
Expand All @@ -380,6 +387,7 @@ func (p *AzureProvider) newRecordSet(endpoint *endpoint.Endpoint) (dns.RecordSet
CnameRecord: &dns.CnameRecord{
Cname: to.Ptr(endpoint.Targets[0]),
},
Metadata: metadata,
},
}, nil
case dns.RecordTypeMX:
Expand All @@ -395,6 +403,7 @@ func (p *AzureProvider) newRecordSet(endpoint *endpoint.Endpoint) (dns.RecordSet
Properties: &dns.RecordSetProperties{
TTL: to.Ptr(ttl),
MxRecords: mxRecords,
Metadata: metadata,
},
}, nil
case dns.RecordTypeNS:
Expand All @@ -408,6 +417,7 @@ func (p *AzureProvider) newRecordSet(endpoint *endpoint.Endpoint) (dns.RecordSet
Properties: &dns.RecordSetProperties{
TTL: to.Ptr(ttl),
NsRecords: nsRecords,
Metadata: metadata,
},
}, nil
case dns.RecordTypeTXT:
Expand All @@ -421,6 +431,7 @@ func (p *AzureProvider) newRecordSet(endpoint *endpoint.Endpoint) (dns.RecordSet
},
},
},
Metadata: metadata,
},
}, nil
}
Expand Down Expand Up @@ -498,3 +509,35 @@ func extractAzureTargets(recordSet *dns.RecordSet) []string {
}
return []string{}
}

// extractMetadataFromRecordSet extracts Azure RecordSet metadata into endpoint ProviderSpecific properties.
func extractMetadataFromRecordSet(ep *endpoint.Endpoint, recordSet *dns.RecordSet) {
if recordSet.Properties == nil || recordSet.Properties.Metadata == nil {
return
}
for key, value := range recordSet.Properties.Metadata {
if value != nil {
ep.WithProviderSpecific(providerSpecificMetadataPrefix+key, *value)
}
}
}

// extractMetadataFromEndpoint extracts Azure metadata from endpoint ProviderSpecific properties.
// Properties with prefix "azure/metadata-" are converted to Azure RecordSet metadata.
func extractMetadataFromEndpoint(ep *endpoint.Endpoint) map[string]*string {
if len(ep.ProviderSpecific) == 0 {
return nil
}
metadata := make(map[string]*string)
for _, ps := range ep.ProviderSpecific {
if key, ok := strings.CutPrefix(ps.Name, providerSpecificMetadataPrefix); ok {
if key != "" && ps.Value != "" {
metadata[key] = to.Ptr(ps.Value)
}
}
}
if len(metadata) == 0 {
return nil
}
return metadata
}
4 changes: 4 additions & 0 deletions provider/azure/azure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ func TestAzureApplyChanges(t *testing.T) {
endpoint.NewEndpointWithTTL("newmail.example.com", endpoint.RecordTypeMX, 7200, "40 bar.other.com"),
endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeMX, endpoint.TTL(recordTTL), "10 other.com"),
endpoint.NewEndpointWithTTL("mail.example.com", endpoint.RecordTypeTXT, endpoint.TTL(recordTTL), "tag"),
endpoint.NewEndpointWithTTL("metadata.example.com", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
})
}

Expand Down Expand Up @@ -439,6 +440,9 @@ func testAzureApplyChangesInternal(t *testing.T, dryRun bool, client RecordSetsC
endpoint.NewEndpoint("nope.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpoint("mail.example.com", endpoint.RecordTypeMX, "10 other.com"),
endpoint.NewEndpoint("mail.example.com", endpoint.RecordTypeTXT, "tag"),
endpoint.NewEndpointWithTTL("metadata.example.com", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").
WithProviderSpecific("azure/metadata-foo", "bar").
WithProviderSpecific("azure/metadata-baz", "qux"),
}

currentRecords := []*endpoint.Endpoint{
Expand Down
2 changes: 2 additions & 0 deletions source/annotations/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ var (
SCWPrefix string
WebhookPrefix string
CloudflarePrefix string
AzurePrefix string

TtlKey string
SetIdentifierKey string
Expand Down Expand Up @@ -85,6 +86,7 @@ func SetAnnotationPrefix(prefix string) {
SCWPrefix = AnnotationKeyPrefix + "scw-"
WebhookPrefix = AnnotationKeyPrefix + "webhook-"
CloudflarePrefix = AnnotationKeyPrefix + "cloudflare-"
AzurePrefix = AnnotationKeyPrefix + "azure-"

// Core annotations
TtlKey = AnnotationKeyPrefix + "ttl"
Expand Down
5 changes: 5 additions & 0 deletions source/annotations/provider_specific.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ func ProviderSpecificAnnotations(annotations map[string]string) (endpoint.Provid
Name: fmt.Sprintf("coredns/%s", attr),
Value: v,
})
} else if attr, ok := strings.CutPrefix(k, AzurePrefix); ok {
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
Name: fmt.Sprintf("azure/%s", attr),
Value: v,
})
} else if strings.HasPrefix(k, CloudflarePrefix) {
switch {
case strings.Contains(k, CloudflareCustomHostnameKey):
Expand Down
30 changes: 30 additions & 0 deletions source/annotations/provider_specific_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@ limitations under the License.
package annotations

import (
"os"
"strconv"
"testing"

"github.com/stretchr/testify/assert"
"sigs.k8s.io/external-dns/endpoint"
)

func TestMain(m *testing.M) {
// Initialize annotation prefixes before running tests
SetAnnotationPrefix(DefaultAnnotationPrefix)
os.Exit(m.Run())
}

func TestProviderSpecificAnnotations(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -74,6 +81,16 @@ func TestProviderSpecificAnnotations(t *testing.T) {
},
setIdentifier: "",
},
{
name: "Azure metadata annotation",
annotations: map[string]string{
"external-dns.alpha.kubernetes.io/azure-metadata-environment": "production",
},
expected: endpoint.ProviderSpecific{
{Name: "azure/metadata-environment", Value: "production"},
},
setIdentifier: "",
},
{
name: "Set identifier annotation",
annotations: map[string]string{
Expand Down Expand Up @@ -358,6 +375,19 @@ func TestGetProviderSpecificIdentifierAnnotations(t *testing.T) {
},
expectedIdentifier: "id1",
},
{
title: "azure- provider specific annotations are set correctly",
annotations: map[string]string{
"external-dns.alpha.kubernetes.io/azure-metadata-foo": "bar",
SetIdentifierKey: "id1",
"external-dns.alpha.kubernetes.io/azure-metadata-baz": "qux",
},
expectedResult: map[string]string{
"azure/metadata-foo": "bar",
"azure/metadata-baz": "qux",
},
expectedIdentifier: "id1",
},
} {
t.Run(tc.title, func(t *testing.T) {
providerSpecificAnnotations, identifier := ProviderSpecificAnnotations(tc.annotations)
Expand Down
Loading