Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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 controller/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ func buildSource(ctx context.Context, cfg *source.Config) (source.Source, error)
wrappers.WithTargetNetFilter(cfg.TargetNetFilter),
wrappers.WithExcludeTargetNets(cfg.ExcludeTargetNets),
wrappers.WithMinTTL(cfg.MinTTL),
wrappers.WithProvider(cfg.Provider),
wrappers.WithPreferAlias(cfg.PreferAlias))
return wrappers.WrapSources(sources, opts)
}
Expand Down
30 changes: 23 additions & 7 deletions docs/contributing/source-wrappers.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ Wrappers solve these key challenges:

## Built In Wrappers

| Wrapper | Purpose | Use Case |
|:--------------------:|:----------------------------------------|:--------------------------------------|
| `MultiSource` | Combine multiple sources. | Aggregate `Ingress`, `Service`, etc. |
| `DedupSource` | Remove duplicate DNS records. | Avoid duplicate records from sources. |
| `TargetFilterSource` | Include/exclude targets based on CIDRs. | Exclude internal IPs. |
| `NAT64Source` | Add NAT64-prefixed AAAA records. | Support IPv6 with NAT64. |
| `PostProcessor` | Add records post-processing. | Configure TTL for all endpoints. |
| Wrapper | Purpose | Use Case |
|:--------------------:|:----------------------------------------|:----------------------------------------------------|
| `MultiSource` | Combine multiple sources. | Aggregate `Ingress`, `Service`, etc. |
| `DedupSource` | Remove duplicate DNS records. | Avoid duplicate records from sources. |
| `TargetFilterSource` | Include/exclude targets based on CIDRs. | Exclude internal IPs. |
| `NAT64Source` | Add NAT64-prefixed AAAA records. | Support IPv6 with NAT64. |
| `PostProcessor` | Add records post-processing. | Configure TTL, filter provider-specific properties. |

### Use Cases

Expand All @@ -56,6 +56,22 @@ Converts IPv4 targets to IPv6 using NAT64 prefixes.
--nat64-prefix=64:ff9b::/96
```

### 3.1 `PostProcessor`

Applies post-processing to all endpoints after they are collected from sources.

📌 **Use case**

- Sets a minimum TTL on endpoints that have no TTL or a TTL below the configured minimum.
- Filters `ProviderSpecific` properties to retain only those belonging to the configured provider (e.g. `aws/evaluate-target-health` when provider is `aws`). Properties with no provider prefix (e.g. `alias`) are considered provider-agnostic and are always retained.
- Sets the `alias=true` provider-specific property on `CNAME` endpoints when `--prefer-alias` is enabled, signalling providers that support ALIAS records (e.g. PowerDNS, AWS) to use them instead of CNAMEs. Per-resource annotations already present are not overwritten.

```yaml
--min-ttl=60s
--provider=aws
--prefer-alias
```

---

## How Wrappers Work
Expand Down
23 changes: 23 additions & 0 deletions endpoint/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ limitations under the License.
package endpoint

import (
"cmp"
"fmt"
"net/netip"
"slices"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -368,6 +370,27 @@ func (e *Endpoint) DeleteProviderSpecificProperty(key string) {
}
}

// RetainProviderProperties retains only properties whose name is prefixed with
// "provider/" (e.g. "aws/evaluate-target-health" for provider "aws").
// Properties belonging to other providers are dropped.
// Properties with no provider prefix (e.g. "alias") are provider-agnostic and always retained.
func (e *Endpoint) RetainProviderProperties(provider string) {
if provider == "" || len(e.ProviderSpecific) == 0 {
return
}
prefix := provider + "/"
result := make(ProviderSpecific, 0, len(e.ProviderSpecific))
for _, prop := range e.ProviderSpecific {
if !strings.Contains(prop.Name, "/") || strings.HasPrefix(prop.Name, prefix) {
result = append(result, prop)
}
}
slices.SortFunc(result, func(a, b ProviderSpecificProperty) int {
return cmp.Compare(a.Name, b.Name)
})
e.ProviderSpecific = result
}

// WithLabel adds or updates a label for the Endpoint.
//
// Example usage:
Expand Down
104 changes: 104 additions & 0 deletions endpoint/endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,110 @@ func TestDeleteProviderSpecificProperty(t *testing.T) {
}
}

func TestRetainProviderProperties(t *testing.T) {
cases := []struct {
name string
endpoint Endpoint
provider string
expected []ProviderSpecificProperty
}{
{
name: "empty provider specific",
endpoint: Endpoint{},
provider: "aws",
expected: nil,
},
{
name: "empty provider, properties untouched",
endpoint: Endpoint{
ProviderSpecific: []ProviderSpecificProperty{
{Name: "aws/evaluate-target-health", Value: "true"},
{Name: "coredns/group", Value: "my-group"},
},
},
provider: "",
expected: []ProviderSpecificProperty{
{Name: "aws/evaluate-target-health", Value: "true"},
{Name: "coredns/group", Value: "my-group"},
},
},
{
name: "all properties match provider",
endpoint: Endpoint{
ProviderSpecific: []ProviderSpecificProperty{
{Name: "aws/evaluate-target-health", Value: "true"},
{Name: "aws/weight", Value: "10"},
},
},
provider: "aws",
expected: []ProviderSpecificProperty{
{Name: "aws/evaluate-target-health", Value: "true"},
{Name: "aws/weight", Value: "10"},
},
},
{
name: "no properties match provider",
endpoint: Endpoint{
ProviderSpecific: []ProviderSpecificProperty{
{Name: "coredns/group", Value: "my-group"},
},
},
provider: "aws",
expected: []ProviderSpecificProperty{},
},
{
name: "mixed providers, only configured provider retained",
endpoint: Endpoint{
ProviderSpecific: []ProviderSpecificProperty{
{Name: "aws/evaluate-target-health", Value: "true"},
{Name: "coredns/group", Value: "my-group"},
{Name: "aws/weight", Value: "10"},
},
},
provider: "aws",
expected: []ProviderSpecificProperty{
{Name: "aws/evaluate-target-health", Value: "true"},
{Name: "aws/weight", Value: "10"},
},
},
{
name: "provider agnostic properties without prefix are retained",
endpoint: Endpoint{
ProviderSpecific: []ProviderSpecificProperty{
{Name: "alias", Value: "true"},
{Name: "aws/evaluate-target-health", Value: "true"},
{Name: "coredns/group", Value: "my-group"},
},
},
provider: "aws",
expected: []ProviderSpecificProperty{
{Name: "alias", Value: "true"},
{Name: "aws/evaluate-target-health", Value: "true"},
},
},
{
name: "provider prefix must match exactly, not as substring",
endpoint: Endpoint{
ProviderSpecific: []ProviderSpecificProperty{
{Name: "aws-extended/some-prop", Value: "val"},
{Name: "aws/weight", Value: "10"},
},
},
provider: "aws",
expected: []ProviderSpecificProperty{
{Name: "aws/weight", Value: "10"},
},
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
c.endpoint.RetainProviderProperties(c.provider)
require.Equal(t, c.expected, []ProviderSpecificProperty(c.endpoint.ProviderSpecific))
})
}
}

func TestFilterEndpointsByOwnerIDWithRecordTypeA(t *testing.T) {
foo1 := &Endpoint{
DNSName: "foo.com",
Expand Down
2 changes: 2 additions & 0 deletions source/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ type Config struct {
GatewayNamespace string
GatewayLabelFilter string
Compatibility string
Provider string
PodSourceDomain string
PublishInternal bool
PublishHostIP bool
Expand Down Expand Up @@ -130,6 +131,7 @@ func NewSourceConfig(cfg *externaldns.Config) *Config {
PodSourceDomain: cfg.PodSourceDomain,
PublishInternal: cfg.PublishInternal,
PublishHostIP: cfg.PublishHostIP,
Provider: cfg.Provider,
AlwaysPublishNotReadyAddresses: cfg.AlwaysPublishNotReadyAddresses,
ConnectorServer: cfg.ConnectorSourceServer,
CRDSourceAPIVersion: cfg.CRDSourceAPIVersion,
Expand Down
16 changes: 15 additions & 1 deletion source/wrappers/post_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package wrappers

import (
"context"
"strings"
"time"

log "github.com/sirupsen/logrus"
Expand All @@ -34,6 +35,7 @@ type postProcessor struct {

type PostProcessorConfig struct {
ttl int64
provider string
preferAlias bool
isConfigured bool
}
Expand All @@ -49,14 +51,25 @@ func WithTTL(ttl time.Duration) PostProcessorOption {
}
}

// WithProviderLabel sets the provider label used to retain provider-specific
// properties on endpoints. Empty or whitespace-only values are ignored.
func WithProviderLabel(input string) PostProcessorOption {
return func(cfg *PostProcessorConfig) {
if p := strings.TrimSpace(input); p != "" {
cfg.isConfigured = true
cfg.provider = p
}
}
}

// WithPostProcessorPreferAlias enables setting alias=true on CNAME endpoints.
// This signals to providers that support ALIAS records (like PowerDNS, AWS)
// to create ALIAS records instead of CNAMEs.
func WithPostProcessorPreferAlias(enabled bool) PostProcessorOption {
return func(cfg *PostProcessorConfig) {
cfg.preferAlias = enabled
if enabled {
cfg.isConfigured = true
cfg.preferAlias = enabled

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No sure about this change. This option with enable=false will do nothing. And is that related to this PR?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, will roll it back. Basically cfg.preferAlias is false be default, so the assigment only make sense when value is true

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

rolled back

}
}
}
Expand Down Expand Up @@ -84,6 +97,7 @@ func (pp *postProcessor) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e
continue
}
ep.WithMinTTL(pp.cfg.ttl)
ep.RetainProviderProperties(pp.cfg.provider)
// Set alias annotation for CNAME records when preferAlias is enabled
// Only set if not already explicitly configured at the source level
if pp.cfg.preferAlias && ep.RecordType == endpoint.RecordTypeCNAME {
Expand Down
Loading
Loading