Skip to content

Commit

Permalink
feat: Add support for ingress and service annotations (#100)
Browse files Browse the repository at this point in the history
* add support for ingress/service annotations

* update example values

* update annotation examples

* update example annotations

* add plan cleanup function

* add some tests

* refactor and add tests

* update `isEndpointMatching` function to assume `match-subdomain` false by default

* cleanup

* update limitations

* update requirements

* update readme

* update readme

* update readme

* update readme

* update readme

* update readme
  • Loading branch information
mircea-pavel-anton authored Oct 29, 2024
1 parent 81f94a0 commit 95f5b64
Show file tree
Hide file tree
Showing 8 changed files with 642 additions and 34 deletions.
20 changes: 9 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,24 @@
[ExternalDNS](https://github.com/kubernetes-sigs/external-dns) is a Kubernetes add-on for automatically managing DNS records for Kubernetes ingresses and services by using different DNS providers. This webhook provider allows you to automate DNS records from your Kubernetes clusters into your MikroTik router.

Supported DNS record types:
Supported DNS record types: `A`, `AAAA`, `CNAME`, `MX`, `NS`, `SRV`, `TXT`

- A
- AAAA
- CNAME
- MX
- NS
- SRV
- TXT
For examples of creating DNS records either via CRDs or via Ingress/Service annotations, check out the [`example/` directory](./example/).

## 🎯 Requirements

- ExternalDNS >= v0.14.0
- Mikrotik RouterOS (tested on 7.14.3 stable)
> [!Note]
> `v0.15.0` of ExternalDNS added support for `providerSpecific` annotations in Ingress/Service objects for webhook providers.
>
> While older versions of ExternalDNS may work, but support for this feature will not be present.
- ExternalDNS >= `v0.15.0`
- Mikrotik RouterOS (tested on `7.16` stable)

## 🚫 Limitations

- Currently, `DNSEndpoints` with multiple `targets` are *technically* not supported. Only one record will be created with the first target from the list, but eDNS will keep trying to update your DNS record in RouterOS, constantly sending `PUT` requests.
- The `Disabled` option on DNS records is currently ignored
- Support for `providerSpecific` annotations on `Ingress` objects is not **yet** supported.

## ⚙️ Configuration Options

Expand Down
4 changes: 3 additions & 1 deletion example/ingress/sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ metadata:
annotations:
external-dns.alpha.kubernetes.io/hostname: "example.com"
external-dns.alpha.kubernetes.io/ttl: "60"
external-dns.alpha.kubernetes.io/webhook-comment: "This is a comment"
external-dns.alpha.kubernetes.io/webhook-comment: "This is a static DNS record created via ingress annotations!"
external-dns.alpha.kubernetes.io/webhook-address-list: "4.5.6.7"
external-dns.alpha.kubernetes.io/webhook-match-subdomain: "false"
spec:
rules:
- host: example.com
Expand Down
1 change: 0 additions & 1 deletion example/records/complex-record.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
---
---
apiVersion: externaldns.k8s.io/v1alpha1
kind: DNSEndpoint
metadata:
Expand Down
6 changes: 4 additions & 2 deletions example/service/service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ metadata:
name: test
annotations:
external-dns.alpha.kubernetes.io/hostname: nginx2.example.com
external-dns.alpha.kubernetes.io/ttl: "600"
external-dns.alpha.kubernetes.io/webhook-comment: "This is a comment"
external-dns.alpha.kubernetes.io/ttl: "1800"
external-dns.alpha.kubernetes.io/webhook-comment: "This is a static DNS record created via service annotations!"
external-dns.alpha.kubernetes.io/webhook-address-list: "6.7.8.9"
external-dns.alpha.kubernetes.io/webhook-match-subdomain: "true"
spec:
ports:
- port: 80
Expand Down
20 changes: 5 additions & 15 deletions example/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
fullnameOverride: external-dns-mikrotik

logLevel: debug
policy: sync
sources: ["ingress", "service"]
logFormat: text
interval: 1s
sources: ["ingress", "service", "crd"]
registry: txt
txtOwnerId: default
txtPrefix: k8s.
domainFilters: ["example.com"]
excludeDomains: []
policy: sync

provider:
name: webhook
Expand Down Expand Up @@ -57,23 +60,10 @@ provider:

extraArgs:
- --ignore-ingress-tls-spec
- --source=crd
- --crd-source-apiversion=externaldns.k8s.io/v1alpha1
- --crd-source-kind=DNSEndpoint
- --managed-record-types=A
- --managed-record-types=AAAA
- --managed-record-types=CNAME
- --managed-record-types=TXT
- --managed-record-types=MX
- --managed-record-types=SRV
- --managed-record-types=NS

rbac:
create: true
additionalPermissions:
- apiGroups: ["externaldns.k8s.io"]
resources: ["dnsendpoints"]
verbs: ["get", "watch", "list"]
- apiGroups: ["externaldns.k8s.io"]
resources: ["dnsendpoints/status"]
verbs: ["*"]
90 changes: 90 additions & 0 deletions internal/mikrotik/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ func (p *MikrotikProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, e

// ApplyChanges applies a given set of changes in the DNS provider.
func (p *MikrotikProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
changes = cleanupChanges(changes)

for _, endpoint := range append(changes.UpdateOld, changes.Delete...) {
if err := p.client.DeleteDNSRecord(endpoint); err != nil {
return err
Expand All @@ -85,3 +87,91 @@ func (p *MikrotikProvider) ApplyChanges(ctx context.Context, changes *plan.Chang
func (p *MikrotikProvider) GetDomainFilter() endpoint.DomainFilterInterface {
return p.domainFilter
}

// ================================================================================================
// UTILS
// ================================================================================================
func getProviderSpecific(ep *endpoint.Endpoint, ps string) string {
value, valueExists := ep.GetProviderSpecificProperty(ps)
if !valueExists {
value, _ = ep.GetProviderSpecificProperty(fmt.Sprintf("webhook/%s", ps))
}
return value
}

func isEndpointMatching(a *endpoint.Endpoint, b *endpoint.Endpoint) bool {
if a.DNSName != b.DNSName || a.Targets[0] != b.Targets[0] || a.RecordTTL != b.RecordTTL {
return false
}

aComment := getProviderSpecific(a, "comment")
bComment := getProviderSpecific(b, "comment")
if aComment != bComment {
return false
}

aMatchSubdomain := getProviderSpecific(a, "match-subdomain")
if aMatchSubdomain == "" {
aMatchSubdomain = "false"
}
bMatchSubdomain := getProviderSpecific(b, "match-subdomain")
if bMatchSubdomain == "" {
bMatchSubdomain = "false"
}
if aMatchSubdomain != bMatchSubdomain {
return false
}

aAddressList := getProviderSpecific(a, "address-list")
bAddressList := getProviderSpecific(b, "address-list")
if aAddressList != bAddressList {
return false
}

aRegexp := getProviderSpecific(a, "regexp")
bRegexp := getProviderSpecific(b, "regexp")
return aRegexp == bRegexp
}

func contains(haystack []*endpoint.Endpoint, needle *endpoint.Endpoint) bool {
for _, v := range haystack {
if isEndpointMatching(needle, v) {
return true
}
}
return false
}

func cleanupChanges(changes *plan.Changes) *plan.Changes {
// Initialize new plan -> we don't really need to worry about Create or Delete changes.
// Only updates are sketchy
newChanges := &plan.Changes{
Create: changes.Create,
Delete: changes.Delete,
UpdateOld: []*endpoint.Endpoint{},
UpdateNew: []*endpoint.Endpoint{},
}

duplicates := []*endpoint.Endpoint{}

for _, old := range changes.UpdateOld {
for _, new := range changes.UpdateNew {
if isEndpointMatching(old, new) {
duplicates = append(duplicates, old)
}
}
}

for _, old := range changes.UpdateOld {
if !contains(duplicates, old) {
newChanges.UpdateOld = append(newChanges.UpdateOld, old)
}
}
for _, new := range changes.UpdateNew {
if !contains(duplicates, new) {
newChanges.UpdateNew = append(newChanges.UpdateNew, new)
}
}

return newChanges
}
Loading

0 comments on commit 95f5b64

Please sign in to comment.