Skip to content
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
75 changes: 75 additions & 0 deletions docs/tutorials/coredns.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,78 @@ dnstools# dig @10.100.4.143 nginx.example.org +short
10.0.2.15
dnstools#
```

## Specific service annotation options

### Groups

Groups can be used to group set of services together. The main use of this is to limit recursion,
i.e. don't return all records, but only a subset. Let's say we have a configuration like this:

```yaml
apiVersion: v1
kind: Service
metadata:
name: a
annotations:
external-dns.alpha.kubernetes.io/hostname: a.domain.local
external-dns.alpha.kubernetes.io/coredns-group: "g1"
spec:
type: LoadBalancer
...
status:
loadBalancer:
ingress:
- ip: 127.0.0.1
---
apiVersion: v1
kind: Service
metadata:
name: b
annotations:
external-dns.alpha.kubernetes.io/hostname: b.domain.local
external-dns.alpha.kubernetes.io/coredns-group: "g1"
spec:
type: LoadBalancer
...
status:
loadBalancer:
ingress:
- ip: 127.0.0.2
---
apiVersion: v1
kind: Service
metadata:
name: c
annotations:
external-dns.alpha.kubernetes.io/hostname: c.subdom.domain.local
external-dns.alpha.kubernetes.io/coredns-group: "g2"
spec:
type: LoadBalancer
...
status:
loadBalancer:
ingress:
- ip: 127.0.0.3
---
apiVersion: v1
kind: Service
metadata:
name: d
annotations:
external-dns.alpha.kubernetes.io/hostname: d.subdom.domain.local
external-dns.alpha.kubernetes.io/coredns-group: "g2"
spec:
type: LoadBalancer
...
status:
loadBalancer:
ingress:
- ip: 127.0.0.4

```

And we want domain.local to return (127.0.0.1 and 127.0.0.2) and subdom.domain.local to return (127.0.0.3 and 127.0.0.4).
For this the two domains, need to be in different groups. What those groups are does not matter,
as long as a and b belong to the same group which is different from the group c and d belong to.
If a service is found without a group it is always included.
11 changes: 10 additions & 1 deletion provider/coredns/coredns.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ const (
priority = 10 // default priority when nothing is set
etcdTimeout = 5 * time.Second

randomPrefixLabel = "prefix"
randomPrefixLabel = "prefix"
providerSpecificGroup = "coredns/group"
)

// coreDNSClient is an interface to work with CoreDNS service records in etcd
Expand Down Expand Up @@ -260,6 +261,9 @@ func (p coreDNSProvider) Records(_ context.Context) ([]*endpoint.Endpoint, error
endpoint.TTL(service.TTL),
service.Host,
)
if service.Group != "" {
ep.WithProviderSpecific(providerSpecificGroup, service.Group)
}
log.Debugf("Creating new ep (%s) with new service host (%s)", ep, service.Host)
}
ep.Labels["originalText"] = service.Text
Expand Down Expand Up @@ -346,12 +350,17 @@ func (p coreDNSProvider) createServicesForEndpoint(dnsName string, ep *endpoint.
prefix = fmt.Sprintf("%08x", rand.Int31())
log.Infof("Generating new prefix: (%s)", prefix)
}
group := ""
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificGroup); ok {
group = prop
}
service := Service{
Host: target,
Text: ep.Labels["originalText"],
Key: p.etcdKeyFor(prefix + "." + dnsName),
TargetStrip: strings.Count(prefix, ".") + 1,
TTL: uint32(ep.RecordTTL),
Group: group,
}
services = append(services, &service)
ep.Labels[target] = prefix
Expand Down
47 changes: 46 additions & 1 deletion provider/coredns/coredns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ func validateServices(services map[string]Service, expectedServices map[string][
}
found := false
for i, expectedServiceEntry := range expectedServiceEntries {
if value.Host == expectedServiceEntry.Host && value.Text == expectedServiceEntry.Text {
if value.Host == expectedServiceEntry.Host && value.Text == expectedServiceEntry.Text && value.Group == expectedServiceEntry.Group {
expectedServiceEntries = append(expectedServiceEntries[:i], expectedServiceEntries[i+1:]...)
found = true
break
Expand Down Expand Up @@ -863,3 +863,48 @@ func TestCoreDNSProvider_updateTXTRecords_ClearsExtraText(t *testing.T) {
assert.Equal(t, "txt-value", services[0].Text)
assert.Empty(t, services[1].Text)
}

func TestApplyChangesAWithGroupServiceTranslation(t *testing.T) {
client := fakeETCDClient{
map[string]Service{},
}
coredns := coreDNSProvider{
client: client,
coreDNSPrefix: defaultCoreDNSPrefix,
}

changes1 := &plan.Changes{
Create: []*endpoint.Endpoint{
endpoint.NewEndpoint("domain1.local", endpoint.RecordTypeA, "5.5.5.5").WithProviderSpecific(providerSpecificGroup, "test1"),
endpoint.NewEndpoint("domain2.local", endpoint.RecordTypeA, "5.5.5.6").WithProviderSpecific(providerSpecificGroup, "test1"),
endpoint.NewEndpoint("domain3.local", endpoint.RecordTypeA, "5.5.5.7").WithProviderSpecific(providerSpecificGroup, "test2"),
},
}
coredns.ApplyChanges(context.Background(), changes1)

expectedServices1 := map[string][]*Service{
"/skydns/local/domain1": {{Host: "5.5.5.5", Group: "test1"}},
"/skydns/local/domain2": {{Host: "5.5.5.6", Group: "test1"}},
"/skydns/local/domain3": {{Host: "5.5.5.7", Group: "test2"}},
}
validateServices(client.services, expectedServices1, t, 1)
}

func TestRecordsAWithGroupServiceTranslation(t *testing.T) {
client := fakeETCDClient{
map[string]Service{
"/skydns/local/domain1": {Host: "5.5.5.5", Group: "test1"},
},
}
coredns := coreDNSProvider{
client: client,
coreDNSPrefix: defaultCoreDNSPrefix,
}
endpoints, err := coredns.Records(context.Background())
require.NoError(t, err)
if prop, ok := endpoints[0].GetProviderSpecificProperty(providerSpecificGroup); !ok {
t.Error("go no Group name")
} else if prop != "test1" {
t.Errorf("got unexpected Group name: %s != %s", prop, "test1")
}
}
1 change: 1 addition & 0 deletions source/annotations/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const (
CloudflareRecordCommentKey = AnnotationKeyPrefix + "cloudflare-record-comment"

AWSPrefix = AnnotationKeyPrefix + "aws-"
CoreDNSPrefix = AnnotationKeyPrefix + "coredns-"
SCWPrefix = AnnotationKeyPrefix + "scw-"
WebhookPrefix = AnnotationKeyPrefix + "webhook-"
CloudflarePrefix = AnnotationKeyPrefix + "cloudflare-"
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 @@ -49,6 +49,11 @@ func ProviderSpecificAnnotations(annotations map[string]string) (endpoint.Provid
Name: fmt.Sprintf("webhook/%s", attr),
Value: v,
})
} else if attr, ok := strings.CutPrefix(k, CoreDNSPrefix); ok {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Missing code coverage. Rest seems ok

Screenshot 2025-09-17 at 16 14 36

providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
Name: fmt.Sprintf("coredns/%s", attr),
Value: v,
})
} else if strings.HasPrefix(k, CloudflarePrefix) {
if strings.Contains(k, CloudflareCustomHostnameKey) {
providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{
Expand Down
10 changes: 10 additions & 0 deletions source/annotations/provider_specific_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ func TestProviderSpecificAnnotations(t *testing.T) {
},
setIdentifier: "",
},
{
name: "CoreDNS annotation",
annotations: map[string]string{
"external-dns.alpha.kubernetes.io/coredns-group": "g1",
},
expected: endpoint.ProviderSpecific{
{Name: "coredns/group", Value: "g1"},
},
setIdentifier: "",
},
{
name: "Set identifier annotation",
annotations: map[string]string{
Expand Down
Loading