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
19 changes: 8 additions & 11 deletions source/contour_httpproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,17 +157,14 @@ func (sc *httpProxySource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, e
}

// apply template if fqdn is missing on HTTPProxy
if (sc.combineFQDNAnnotation || len(hpEndpoints) == 0) && sc.fqdnTemplate != nil {
tmplEndpoints, err := sc.endpointsFromTemplate(hp)
if err != nil {
return nil, fmt.Errorf("failed to get endpoints from template: %w", err)
}

if sc.combineFQDNAnnotation {
hpEndpoints = append(hpEndpoints, tmplEndpoints...)
} else {
hpEndpoints = tmplEndpoints
}
hpEndpoints, err = fqdn.CombineWithTemplatedEndpoints(
hpEndpoints,
sc.fqdnTemplate,
sc.combineFQDNAnnotation,
func() ([]*endpoint.Endpoint, error) { return sc.endpointsFromTemplate(hp) },
)
if err != nil {
return nil, err
}

if len(hpEndpoints) == 0 {
Expand Down
35 changes: 35 additions & 0 deletions source/fqdn/fqdn.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"

"sigs.k8s.io/external-dns/endpoint"
)

func ParseTemplate(input string) (*template.Template, error) {
Expand Down Expand Up @@ -94,3 +96,36 @@ func isIPv4String(target string) bool {
}
return netIP.Is4()
}

// CombineWithTemplatedEndpoints merges annotation-based endpoints with template-based endpoints
// according to the FQDN template configuration.
//
// Logic:
// - If fqdnTemplate is nil, returns original endpoints unchanged
// - If combineFQDNAnnotation is true, appends templated endpoints to existing
// - If combineFQDNAnnotation is false and endpoints is empty, uses templated endpoints
// - If combineFQDNAnnotation is false and endpoints exist, returns original unchanged
func CombineWithTemplatedEndpoints(
endpoints []*endpoint.Endpoint,
fqdnTemplate *template.Template,
combineFQDNAnnotation bool,
templateFunc func() ([]*endpoint.Endpoint, error),
) ([]*endpoint.Endpoint, error) {
if fqdnTemplate == nil {
return endpoints, nil
}

if !combineFQDNAnnotation && len(endpoints) > 0 {
return endpoints, nil
}

templatedEndpoints, err := templateFunc()
if err != nil {
return nil, fmt.Errorf("failed to get endpoints from template: %w", err)
}

if combineFQDNAnnotation {
return append(endpoints, templatedEndpoints...), nil
}
return templatedEndpoints, nil
}
104 changes: 104 additions & 0 deletions source/fqdn/fqdn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ limitations under the License.
package fqdn

import (
"errors"
"testing"
"text/template"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"

"sigs.k8s.io/external-dns/endpoint"
)

func TestParseTemplate(t *testing.T) {
Expand Down Expand Up @@ -431,3 +435,103 @@ func TestExecTemplateExecutionError(t *testing.T) {
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to apply template on TestKind default/test-name")
}

func TestCombineWithTemplatedEndpoints(t *testing.T) {
// Create a dummy template for tests that need one
dummyTemplate := template.Must(template.New("test").Parse("{{.Name}}"))

annotationEndpoints := []*endpoint.Endpoint{
endpoint.NewEndpoint("annotation.example.com", endpoint.RecordTypeA, "1.2.3.4"),
}
templatedEndpoints := []*endpoint.Endpoint{
endpoint.NewEndpoint("template.example.com", endpoint.RecordTypeA, "5.6.7.8"),
}

successTemplateFunc := func() ([]*endpoint.Endpoint, error) {
return templatedEndpoints, nil
}
errorTemplateFunc := func() ([]*endpoint.Endpoint, error) {
return nil, errors.New("template error")
}

tests := []struct {
name string
endpoints []*endpoint.Endpoint
fqdnTemplate *template.Template
combineFQDNAnnotation bool
templateFunc func() ([]*endpoint.Endpoint, error)
want []*endpoint.Endpoint
wantErr bool
}{
{
name: "nil template returns original endpoints",
endpoints: annotationEndpoints,
fqdnTemplate: nil,
templateFunc: successTemplateFunc,
want: annotationEndpoints,
},
{
name: "combine=false with existing endpoints returns original",
endpoints: annotationEndpoints,
fqdnTemplate: dummyTemplate,
templateFunc: successTemplateFunc,
want: annotationEndpoints,
},
{
name: "combine=false with empty endpoints returns templated",
endpoints: []*endpoint.Endpoint{},
fqdnTemplate: dummyTemplate,
templateFunc: successTemplateFunc,
want: templatedEndpoints,
},
{
name: "combine=true appends templated to existing",
endpoints: annotationEndpoints,
fqdnTemplate: dummyTemplate,
combineFQDNAnnotation: true,
templateFunc: successTemplateFunc,
want: append(annotationEndpoints, templatedEndpoints...),
},
{
name: "combine=true with empty endpoints returns templated",
endpoints: []*endpoint.Endpoint{},
fqdnTemplate: dummyTemplate,
combineFQDNAnnotation: true,
templateFunc: successTemplateFunc,
want: templatedEndpoints,
},
{
name: "template error is propagated",
endpoints: []*endpoint.Endpoint{},
fqdnTemplate: dummyTemplate,
templateFunc: errorTemplateFunc,
want: nil,
wantErr: true,
},
{
name: "nil endpoints with combine=false returns templated",
endpoints: nil,
fqdnTemplate: dummyTemplate,
templateFunc: successTemplateFunc,
want: templatedEndpoints,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := CombineWithTemplatedEndpoints(
tt.endpoints,
tt.fqdnTemplate,
tt.combineFQDNAnnotation,
tt.templateFunc,
)
if tt.wantErr {
require.Error(t, err)
require.ErrorContains(t, err, "failed to get endpoints from template")
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
15 changes: 8 additions & 7 deletions source/ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,14 @@ func (sc *ingressSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, err
ingEndpoints := endpointsFromIngress(ing, sc.ignoreHostnameAnnotation, sc.ignoreIngressTLSSpec, sc.ignoreIngressRulesSpec)

// apply template if host is missing on ingress
if (sc.combineFQDNAnnotation || len(ingEndpoints) == 0) && sc.fqdnTemplate != nil {
iEndpoints, err := sc.endpointsFromTemplate(ing)
if err != nil {
return nil, err
}

ingEndpoints = append(ingEndpoints, iEndpoints...)
ingEndpoints, err = fqdn.CombineWithTemplatedEndpoints(
ingEndpoints,
sc.fqdnTemplate,
sc.combineFQDNAnnotation,
func() ([]*endpoint.Endpoint, error) { return sc.endpointsFromTemplate(ing) },
)
if err != nil {
return nil, err
}

if len(ingEndpoints) == 0 {
Expand Down
34 changes: 16 additions & 18 deletions source/istio_gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,28 +166,26 @@ func (sc *gatewaySource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e
return nil, err
}

// apply template if host is missing on gateway
if (sc.combineFQDNAnnotation || len(gwHostnames) == 0) && sc.fqdnTemplate != nil {
iHostnames, err := fqdn.ExecTemplate(sc.fqdnTemplate, gateway)
if err != nil {
return nil, err
}

if sc.combineFQDNAnnotation {
gwHostnames = append(gwHostnames, iHostnames...)
} else {
gwHostnames = iHostnames
}
}

log.Debugf("Processing gateway '%s/%s.%s' and hosts %q", gateway.Namespace, gateway.APIVersion, gateway.Name, strings.Join(gwHostnames, ","))

if len(gwHostnames) == 0 {
log.Debugf("No hostnames could be generated from gateway %s/%s", gateway.Namespace, gateway.Name)
continue
gwEndpoints, err := sc.endpointsFromGateway(ctx, gwHostnames, gateway)
if err != nil {
return nil, err
}

gwEndpoints, err := sc.endpointsFromGateway(ctx, gwHostnames, gateway)
// apply template if host is missing on gateway
gwEndpoints, err = fqdn.CombineWithTemplatedEndpoints(
gwEndpoints,
sc.fqdnTemplate,
sc.combineFQDNAnnotation,
func() ([]*endpoint.Endpoint, error) {
hostnames, err := fqdn.ExecTemplate(sc.fqdnTemplate, gateway)
if err != nil {
return nil, err
}
return sc.endpointsFromGateway(ctx, hostnames, gateway)
},
)
if err != nil {
return nil, err
}
Expand Down
19 changes: 8 additions & 11 deletions source/istio_virtualservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,17 +171,14 @@ func (sc *virtualServiceSource) Endpoints(ctx context.Context) ([]*endpoint.Endp
}

// apply template if host is missing on VirtualService
if (sc.combineFQDNAnnotation || len(gwEndpoints) == 0) && sc.fqdnTemplate != nil {
iEndpoints, err := sc.endpointsFromTemplate(ctx, vService)
if err != nil {
return nil, err
}

if sc.combineFQDNAnnotation {
gwEndpoints = append(gwEndpoints, iEndpoints...)
} else {
gwEndpoints = iEndpoints
}
gwEndpoints, err = fqdn.CombineWithTemplatedEndpoints(
gwEndpoints,
sc.fqdnTemplate,
sc.combineFQDNAnnotation,
func() ([]*endpoint.Endpoint, error) { return sc.endpointsFromTemplate(ctx, vService) },
)
if err != nil {
return nil, err
}

if len(gwEndpoints) == 0 {
Expand Down
Loading
Loading