Skip to content

feat: add isSource template function to scope --fqdn-template by source - #6625

Open
ivankatliarchuk wants to merge 12 commits into
kubernetes-sigs:masterfrom
gofogo:feat/fqdn-template-isSource-scoping
Open

feat: add isSource template function to scope --fqdn-template by source#6625
ivankatliarchuk wants to merge 12 commits into
kubernetes-sigs:masterfrom
gofogo:feat/fqdn-template-isSource-scoping

Conversation

@ivankatliarchuk

@ivankatliarchuk ivankatliarchuk commented Aug 11, 2026

Copy link
Copy Markdown
Member

What does it do ?

  • New case-insensitive template function isSource "name" (matches the exact --source flag value, e.g. "traefik-proxy"), usable in --fqdn-template/--target-template/--fqdn-target-template.
  • template.Engine gains WithSource(name), which scopes an engine to one source by cloning its parsed templates and rebinding isSource — avoids re-parsing and avoids mutating a template shared across other sources' engines.
  • Docs: new isSource section and function-table entry in docs/advanced/fqdn-templating.md, explaining the source-vs-Kind distinction with a worked example.

Motivation

Adds a way to scope templates to specific sources. Relates #6593

  • Users running multiple ExternalDNS sources (service, node, traefik-proxy, ...) with a single --fqdn-template had no way to render different logic per source.
  • The existing {{ eq .Kind "Service" }} pattern doesn't work reliably for this: some sources span multiple Kubernetes Kinds under one source name (traefik-proxy → IngressRoute/IngressRouteTCP/IngressRouteUDP; unstructured → arbitrary CRD kinds), so Kind-based conditionals can't cleanly target "this source" as a whole.

Complement a solution like #6611

More

  • Yes, this PR title follows Conventional Commits
  • Yes, I added unit tests
  • Yes, I updated end user documentation accordingly

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
@kubernetes-prow kubernetes-prow Bot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/L Denotes a PR that changes 100-499 lines, ignoring generated files. labels Aug 11, 2026
@kubernetes-prow
kubernetes-prow Bot requested a review from szuecs August 11, 2026 08:52
@kubernetes-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign ivankatliarchuk for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubernetes-prow kubernetes-prow Bot added the docs label Aug 11, 2026
@kubernetes-prow
kubernetes-prow Bot requested a review from vflaux August 11, 2026 08:52
Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
@coveralls

coveralls commented Aug 11, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 32010655162

Coverage decreased (-0.008%) to 81.849%

Details

  • Coverage decreased (-0.008%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 82 coverage regressions across 3 files.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

82 previously-covered lines in 3 files lost coverage.

File Lines Losing Coverage Coverage
store.go 41 77.09%
template/engine.go 40 65.75%
integration/toolkit/toolkit.go 1 82.32%

Coverage Stats

Coverage Status
Relevant Lines: 21459
Covered Lines: 17564
Line Coverage: 81.85%
Coverage Strength: 1453.79 hits per line

💛 - Coveralls

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
@mloiseleur mloiseleur changed the title fieat: Add isSource template function to scope --fqdn-template by source feat: add isSource template function to scope --fqdn-template by source Aug 11, 2026
Comment thread source/template/engine.go Outdated
Comment on lines +53 to +55
// source is the ExternalDNS source name (e.g. "service", "traefik-proxy") this Engine
// was scoped to via WithSource. Empty for an unscoped Engine.
source string

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// source is the ExternalDNS source name (e.g. "service", "traefik-proxy") this Engine
// was scoped to via WithSource. Empty for an unscoped Engine.
source string

Unless I'm missing something, this parameter is written but never read nor used.

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.

It was assigned, but not used. Pushed change

Comment thread source/template/functions.go Outdated
"isIPv4": isIPv4,
"hasKey": hasKey,
"fromJson": fromJson,
// stub: the source isn't known at parse time; Engine.WithSource rebinds this per source.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It may makes unknown source name indistinguishable from "not this source".
Wdyt of adding a validation on this ?
Something like this, in source/template/validate.go:

  func validateIsSourceArgs(tmpl *template.Template, flag string) error {
        unknown := sets.New[string]()
        for _, t := range tmpl.Templates() {
                if t.Tree == nil {
                        continue
                }
                walkCommands(t.Root, func(cmd *parse.CommandNode) {
                        if len(cmd.Args) == 0 {
                                return
                        }
                        id, ok := cmd.Args[0].(*parse.IdentifierNode)
                        if !ok || id.Ident != "isSource" {
                                return
                        }
                        for _, arg := range cmd.Args[1:] {
                                if s, ok := arg.(*parse.StringNode); ok && !types.IsKnown(s.Text) {
                                        unknown.Insert(s.Text)
                                }
                        }
                })
        }
        if len(unknown) == 0 {
                return nil
        }
        return fmt.Errorf("parse %s: isSource: unknown source %q (valid: %s)",
                flag, strings.Join(sets.Sorted(unknown), `", "`), strings.Join(types.All, ", "))
  }

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.

We could validate values, sanitize templates against values. For walkCommands - a correct walk has to recurse into IfNode/RangeNode/WithNode/TemplateNode bodies and nested structs, not just top-level t.Root commands - {{ if eq (isSource "ingres") true }} as example needs to be caught too and many more cases. A shallow walker gives false confidence (validates the easy cases, misses nested ones). I understand the gain, is just looks a bit too complex to correctly resolve.

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.

Added validation directly to the function for now

Comment thread tests/integration/scenarios/tests.yaml
ivankatliarchuk and others added 5 commits August 14, 2026 09:33
Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com>
Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
…isSource-scoping' into feat/fqdn-template-isSource-scoping

* refs/remotes/origin/feat/fqdn-template-isSource-scoping:
  feat: Add isSource template function to scope --fqdn-template by source
Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
Comment thread source/store.go
func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg *Config) (Source, error) {
// Scope the template engine to this source so templates can use isSource "name".
var err error
if cfg.TemplateEngine, err = cfg.TemplateEngine.WithSource(source); err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It's modifying the Config, so it could get stuck on last source name, when called multiples times.
FTM, we are good, but we may need at some point to snapshot & restore on defer

Comment on lines +63 to +73
func TestAllContainsEveryConstant(t *testing.T) {
expected := []Type{
Node, Service, Ingress, Pod,
GatewayHttpRoute, GatewayGrpcRoute, GatewayTlsRoute, GatewayTcpRoute, GatewayUdpRoute,
IstioGateway, IstioVirtualService,
AmbassadorHost, ContourHTTPProxy, GlooProxy, TraefikProxy, OpenShiftRoute,
Fake, Connector, CRD, SkipperRouteGroup, KongTCPIngress,
F5VirtualServer, F5TransportServer, Unstructured,
}
assert.ElementsMatch(t, expected, All)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This test compares a hand-copy in types.go to a hand-copy in this file.
Mmmh 🤔
@ivankatliarchuk Maybe we can do better by ensuring we are aligned between the source code and the CLI code. When we add a new source, we definitely do not want to miss to fill the two lists (All in source/types/type.go and the allowedSources in pkg/apis/externaldns/types.go.

It could be something like that in pkg/apis/externaldns/types_test.go:

  func TestAllowedSourcesMatchesSourceTypes(t *testing.T) {
        want := append(slices.Clone(types.All), "empty")
        slices.Sort(want)
        got := slices.Clone(allowedSources)
        slices.Sort(got)
        assert.Equal(t, want, got, "allowedSources and source/types.All have drifted")
  }

@ivankatliarchuk Wdyt ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. docs size/L Denotes a PR that changes 100-499 lines, ignoring generated files. source

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants