Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for ingress and service annotations #100

Merged
merged 17 commits into from
Oct 29, 2024
Merged
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
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