/* Copyright 2026 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package source import ( "testing" "sigs.k8s.io/external-dns/internal/testutils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" templatetest "sigs.k8s.io/external-dns/source/template/testutil" ) func TestUnstructuredFqdnTemplatingExamples(t *testing.T) { type cfg struct { resources []string fqdnTemplate string targetTemplate string fqdnTargetTemplate string labelFilter string combine bool } for _, tt := range []struct { title string cfg cfg objects []*unstructured.Unstructured expected []*endpoint.Endpoint }{ { title: "ConfigMap with comma-separated hostnames", cfg: cfg{ resources: []string{"configmaps.v1"}, fqdnTemplate: `{{index .Object.data "hostnames"}}`, targetTemplate: `{{index .Object.data "target"}}`, }, objects: []*unstructured.Unstructured{ { Object: map[string]any{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]any{ "name": "multi-dns", "namespace": "default", "annotations": map[string]any{ annotations.ControllerKey: annotations.ControllerValue, }, }, "data": map[string]any{ "hostnames": "entry1.internal.tld,entry2.example.tld", "target": "10.10.10.10", }, }, }, }, expected: []*endpoint.Endpoint{ endpoint.NewEndpoint("entry1.internal.tld", endpoint.RecordTypeA, "10.10.10.10"). WithLabel(endpoint.ResourceLabelKey, "configmap/default/multi-dns"), endpoint.NewEndpoint("entry2.example.tld", endpoint.RecordTypeA, "10.10.10.10"). WithLabel(endpoint.ResourceLabelKey, "configmap/default/multi-dns"), }, }, { title: "with IP address", cfg: cfg{ resources: []string{"virtualmachineinstances.v1.kubevirt.io"}, fqdnTemplate: `{{.Name}}.{{index .Status.interfaces 0 "name"}}.vmi.com`, targetTemplate: `{{index .Status.interfaces 0 "ipAddress"}}`, }, objects: []*unstructured.Unstructured{ { Object: map[string]any{ "apiVersion": "kubevirt.io/v1", "kind": "VirtualMachineInstance", "metadata": map[string]any{ "name": "my-vm", "namespace": "default", }, "status": map[string]any{ "interfaces": []any{ map[string]any{ "ipAddress": "10.244.1.50", "name": "main", }, }, }, }, }, }, expected: []*endpoint.Endpoint{ endpoint.NewEndpoint("my-vm.main.vmi.com", endpoint.RecordTypeA, "10.244.1.50"). WithLabel(endpoint.ResourceLabelKey, "virtualmachineinstance/default/my-vm"), }, }, { title: "Crossplane RDSInstance with endpoint", cfg: cfg{ resources: []string{"rdsinstances.v1alpha1.rds.aws.crossplane.io"}, fqdnTemplate: "{{.Name}}.db.example.com", targetTemplate: "{{.Status.atProvider.endpoint.address}}", }, objects: []*unstructured.Unstructured{ { Object: map[string]any{ "apiVersion": "rds.aws.crossplane.io/v1alpha1", "kind": "RDSInstance", "metadata": map[string]any{ "name": "prod-postgres", "namespace": "default", }, "status": map[string]any{ "atProvider": map[string]any{ "endpoint": map[string]any{ "address": "prod-postgres.abc123.us-east-1.rds", }, }, }, }, }, }, expected: []*endpoint.Endpoint{ endpoint.NewEndpoint("prod-postgres.db.example.com", endpoint.RecordTypeCNAME, "prod-postgres.abc123.us-east-1.rds."). WithLabel(endpoint.ResourceLabelKey, "rdsinstance/default/prod-postgres"), }, }, { title: "multiple VirtualMachineInstances", cfg: cfg{ resources: []string{"virtualmachineinstances.v1.kubevirt.io"}, fqdnTemplate: "{{.Name}}.vmi.example.com", targetTemplate: `{{index .Status.interfaces 0 "ipAddress"}}`, }, objects: []*unstructured.Unstructured{ { Object: map[string]any{ "apiVersion": "kubevirt.io/v1", "kind": "VirtualMachineInstance", "metadata": map[string]any{ "name": "vm-one", "namespace": "default", }, "status": map[string]any{ "interfaces": []any{ map[string]any{"ipAddress": "10.244.1.10"}, }, }, }, }, { Object: map[string]any{ "apiVersion": "kubevirt.io/v1", "kind": "VirtualMachineInstance", "metadata": map[string]any{ "name": "vm-two", "namespace": "default", }, "status": map[string]any{ "interfaces": []any{ map[string]any{"ipAddress": "10.244.1.20"}, }, }, }, }, }, expected: []*endpoint.Endpoint{ endpoint.NewEndpoint("vm-one.vmi.example.com", endpoint.RecordTypeA, "10.244.1.10"). WithLabel(endpoint.ResourceLabelKey, "virtualmachineinstance/default/vm-one"), endpoint.NewEndpoint("vm-two.vmi.example.com", endpoint.RecordTypeA, "10.244.1.20"). WithLabel(endpoint.ResourceLabelKey, "virtualmachineinstance/default/vm-two"), }, }, { title: "multiple hosts from template", cfg: cfg{ resources: []string{"proxyservices.v1beta1.proxyconfigs.acme.corp"}, fqdnTemplate: "{{.Name}}.mesh.com,{{.Name}}.internal.com", targetTemplate: "{{index .Spec.hosts 0}}", }, objects: []*unstructured.Unstructured{ { Object: map[string]any{ "apiVersion": "proxyconfigs.acme.corp/v1beta1", "kind": "ProxyService", "metadata": map[string]any{ "name": "reviews", "namespace": "default", }, "spec": map[string]any{ "hosts": []any{ "promo.svc.local", }, }, }, }, }, expected: []*endpoint.Endpoint{ endpoint.NewEndpoint("reviews.internal.com", endpoint.RecordTypeCNAME, "promo.svc.local"). WithLabel(endpoint.ResourceLabelKey, "proxyservice/default/reviews"), endpoint.NewEndpoint("reviews.mesh.com", endpoint.RecordTypeCNAME, "promo.svc.local"). WithLabel(endpoint.ResourceLabelKey, "proxyservice/default/reviews"), }, }, { title: "with labels", cfg: cfg{ resources: []string{"applications.v1alpha1.argoproj.io"}, fqdnTemplate: `{{index .Labels "app.kubernetes.io/instance"}}.apps.com`, targetTemplate: "{{.Status.loadBalancer}}", }, objects: []*unstructured.Unstructured{ { Object: map[string]any{ "apiVersion": "argoproj.io/v1alpha1", "kind": "Application", "metadata": map[string]any{ "name": "guestbook", "namespace": "default", "labels": map[string]any{ "app.kubernetes.io/instance": "guestbook-prod", }, }, "status": map[string]any{ "loadBalancer": "lb.example.com", }, }, }, }, expected: []*endpoint.Endpoint{ endpoint.NewEndpoint("guestbook-prod.apps.com", endpoint.RecordTypeCNAME, "lb.example.com"). WithLabel(endpoint.ResourceLabelKey, "application/default/guestbook"), }, }, { title: "with ttl annotation set", cfg: cfg{ resources: []string{"applications.v1alpha1.argoproj.io"}, fqdnTemplate: `{{index .Labels "app.kubernetes.io/instance"}}.apps.com`, targetTemplate: "{{.Status.loadBalancer}}", }, objects: []*unstructured.Unstructured{ { Object: map[string]any{ "apiVersion": "argoproj.io/v1alpha1", "kind": "Application", "metadata": map[string]any{ "name": "guestbook", "namespace": "ns", "labels": map[string]any{ "app.kubernetes.io/instance": "guestbook-prod", }, "annotations": map[string]any{ annotations.TtlKey: "300", }, }, "status": map[string]any{ "loadBalancer": "lb.example.com", }, }, }, }, expected: []*endpoint.Endpoint{ endpoint.NewEndpointWithTTL("guestbook-prod.apps.com", endpoint.RecordTypeCNAME, 300, "lb.example.com"). WithLabel(endpoint.ResourceLabelKey, "application/ns/guestbook"), }, }, { title: "two different resource types - VirtualMachineInstance and RDSInstance", cfg: cfg{ resources: []string{ "virtualmachineinstances.v1.kubevirt.io", "rdsinstances.v1alpha1.rds.aws.crossplane.io", }, fqdnTemplate: "{{.Name}}.{{.Namespace}}.com", targetTemplate: ` {{if .Status.interfaces}}{{index .Status.interfaces 0 "ipAddress"}}{{else}}{{.Status.atProvider.endpoint.address}}{{end}}`, }, objects: []*unstructured.Unstructured{ { Object: map[string]any{ "apiVersion": "kubevirt.io/v1", "kind": "VirtualMachineInstance", "metadata": map[string]any{ "name": "my-vm", "namespace": "vms", }, "status": map[string]any{ "interfaces": []any{ map[string]any{ "ipAddress": "10.244.1.100", }, }, }, }, }, { Object: map[string]any{ "apiVersion": "rds.aws.crossplane.io/v1alpha1", "kind": "RDSInstance", "metadata": map[string]any{ "name": "my-db", "namespace": "databases", }, "status": map[string]any{ "atProvider": map[string]any{ "endpoint": map[string]any{ "address": "my-db.abc123.us-west-2.rds", }, }, }, }, }, }, expected: []*endpoint.Endpoint{ endpoint.NewEndpoint("my-db.databases.com", endpoint.RecordTypeCNAME, "my-db.abc123.us-west-2.rds"). WithLabel(endpoint.ResourceLabelKey, "rdsinstance/databases/my-db"), endpoint.NewEndpoint("my-vm.vms.com", endpoint.RecordTypeA, "10.244.1.100"). WithLabel(endpoint.ResourceLabelKey, "virtualmachineinstance/vms/my-vm"), }, }, { title: "two different resource types with same template", cfg: cfg{ resources: []string{ "virtualmachineinstances.v1.kubevirt.io", "targetgroupbindings.v1beta1.elbv2.k8s.aws", }, fqdnTemplate: "{{.Name}}.{{.Kind}}.example.com", targetTemplate: "{{.Status.target}}", }, objects: []*unstructured.Unstructured{ { Object: map[string]any{ "apiVersion": "kubevirt.io/v1", "kind": "VirtualMachineInstance", "metadata": map[string]any{ "name": "web-server", "namespace": "default", }, "status": map[string]any{ "target": "192.168.1.10", }, }, }, { Object: map[string]any{ "apiVersion": "elbv2.k8s.aws/v1beta1", "kind": "TargetGroupBinding", "metadata": map[string]any{ "name": "api-tgb", "namespace": "default", }, "status": map[string]any{ "target": "lb.api.example.com", }, }, }, }, expected: []*endpoint.Endpoint{ endpoint.NewEndpoint("api-tgb.TargetGroupBinding.example.com", endpoint.RecordTypeCNAME, "lb.api.example.com"). WithLabel(endpoint.ResourceLabelKey, "targetgroupbinding/default/api-tgb"), endpoint.NewEndpoint("web-server.VirtualMachineInstance.example.com", endpoint.RecordTypeA, "192.168.1.10"). WithLabel(endpoint.ResourceLabelKey, "virtualmachineinstance/default/web-server"), }, }, { title: "combined annotations and template", cfg: cfg{ resources: []string{"virtualmachineinstances.v1.kubevirt.io"}, fqdnTemplate: "{{.Name}}.template.example.com", targetTemplate: `{{index .Status.interfaces 0 "ipAddress"}}`, combine: true, }, objects: []*unstructured.Unstructured{ { Object: map[string]any{ "apiVersion": "kubevirt.io/v1", "kind": "VirtualMachineInstance", "metadata": map[string]any{ "name": "my-vm", "namespace": "default", "annotations": map[string]any{ annotations.HostnameKey: "my-vm.annotation.example.com", annotations.TargetKey: "192.168.1.100", }, }, "status": map[string]any{ "interfaces": []any{ map[string]any{ "ipAddress": "10.244.1.50", }, }, }, }, }, }, expected: []*endpoint.Endpoint{ endpoint.NewEndpoint("my-vm.annotation.example.com", endpoint.RecordTypeA, "192.168.1.100"). WithLabel(endpoint.ResourceLabelKey, "virtualmachineinstance/default/my-vm"), endpoint.NewEndpoint("my-vm.template.example.com", endpoint.RecordTypeA, "10.244.1.50"). WithLabel(endpoint.ResourceLabelKey, "virtualmachineinstance/default/my-vm"), }, }, { title: "three different resource types", cfg: cfg{ resources: []string{ "virtualmachineinstances.v1.kubevirt.io", "targetgroupbinding.v1beta1.elbv2.k8s.aws", "apisixroute.v2.apisix.apache.org", }, fqdnTemplate: ` {{if eq .Kind "VirtualMachineInstance"}}{{.Name}}.vm.com{{end}}, {{if eq .Kind "TargetGroupBinding"}}{{.Name}}.tgb.com{{end}}, {{if eq .Kind "ApisixRoute"}}{{.Name}}.route.com{{end}}`, targetTemplate: ` {{if eq .Kind "VirtualMachineInstance"}}{{index .Status.interfaces 0 "ipAddress"}}{{end}}, {{if eq .Kind "TargetGroupBinding"}}{{.Status.loadBalancerHostname}}{{end}}, {{if eq .Kind "ApisixRoute"}}{{.Status.apisix.gateway}}{{end}}`, }, objects: []*unstructured.Unstructured{ { Object: map[string]any{ "apiVersion": "kubevirt.io/v1", "kind": "VirtualMachineInstance", "metadata": map[string]any{ "name": "my-vm", "namespace": "vms", }, "status": map[string]any{ "interfaces": []any{ map[string]any{ "ipAddress": "10.0.0.1", "name": "default", }, }, }, }, }, { Object: map[string]any{ "apiVersion": "elbv2.k8s.aws/v1beta1", "kind": "TargetGroupBinding", "metadata": map[string]any{ "name": "my-tgb", "namespace": "apps", }, "status": map[string]any{ "loadBalancerHostname": "my-alb.us-east-1.elb.amazonaws.com", }, }, }, { Object: map[string]any{ "apiVersion": "apisix.apache.org/v2", "kind": "ApisixRoute", "metadata": map[string]any{ "name": "httpbin", "namespace": "ingress-apisix", }, "spec": map[string]any{ "ingressClassName": "apisix", "http": []any{ map[string]any{ "name": "httpbin", "match": map[string]any{ "paths": []any{"/ip"}, }, "backends": []any{ map[string]any{ "serviceName": "httpbin", "servicePort": int64(80), }, }, }, }, }, "status": map[string]any{ "apisix": map[string]any{ "gateway": "apisix-gateway.ingress-apisix.svc.cluster.local", }, }, }, }, }, expected: []*endpoint.Endpoint{ endpoint.NewEndpoint("httpbin.route.com", endpoint.RecordTypeCNAME, "apisix-gateway.ingress-apisix.svc.cluster.local"). WithLabel(endpoint.ResourceLabelKey, "apisixroute/ingress-apisix/httpbin"), endpoint.NewEndpoint("my-tgb.tgb.com", endpoint.RecordTypeCNAME, "my-alb.us-east-1.elb.amazonaws.com"). WithLabel(endpoint.ResourceLabelKey, "targetgroupbinding/apps/my-tgb"), endpoint.NewEndpoint("my-vm.vm.com", endpoint.RecordTypeA, "10.0.0.1"). WithLabel(endpoint.ResourceLabelKey, "virtualmachineinstance/vms/my-vm"), }, }, { title: "ACK S3 Bucket with FieldExport to ConfigMap", cfg: cfg{ resources: []string{ "buckets.v1alpha1.s3.services.k8s.aws", "fieldexports.v1alpha1.services.k8s.aws", "configmap.v1"}, fqdnTemplate: `{{if eq .Kind "ConfigMap"}}{{.Name}}.s3.example.com{{end}}`, targetTemplate: ` {{if eq .Kind "ConfigMap"}}{{$url := index .Object.data "default.export-user-data-bucket"}}{{trimSuffix (trimPrefix $url "https://") "/"}}{{end}}`, labelFilter: "app.kubernetes.io/name=example-app", }, objects: []*unstructured.Unstructured{ { Object: map[string]any{ "apiVersion": "s3.services.k8s.aws/v1alpha1", "kind": "Bucket", "metadata": map[string]any{ "name": "application-user-data", "namespace": "default", "labels": map[string]any{ "app.kubernetes.io/name": "example-app", "app.kubernetes.io/part-of": "exported-config", }, "annotations": map[string]any{ annotations.ControllerKey: annotations.ControllerValue, }, }, "spec": map[string]any{ "name": "doc-example-bucket", }, }, }, { Object: map[string]any{ "apiVersion": "services.k8s.aws/v1alpha1", "kind": "FieldExport", "metadata": map[string]any{ "name": "export-user-data-bucket", "namespace": "default", "labels": map[string]any{ "app.kubernetes.io/name": "example-app", "app.kubernetes.io/part-of": "exported-config", }, "annotations": map[string]any{ annotations.ControllerKey: annotations.ControllerValue, }, }, "spec": map[string]any{ "to": map[string]any{ "name": "application-user-data-cm", "namespace": "default", "kind": "configmap", }, "from": map[string]any{ "path": ".status.location", "resource": map[string]any{ "group": "s3.services.k8s.aws", "kind": "Bucket", "name": "application-user-data", "namespace": "default", }, }, }, }, }, { Object: map[string]any{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]any{ "name": "application-user-data-cm", "namespace": "default", "labels": map[string]any{ "app.kubernetes.io/name": "example-app", "app.kubernetes.io/part-of": "exported-config", }, "annotations": map[string]any{ annotations.ControllerKey: annotations.ControllerValue, }, }, "data": map[string]any{ "default.export-user-data-bucket": "https://doc-example-bucket.s3.amazonaws.com/", }, }, }, }, expected: []*endpoint.Endpoint{ endpoint.NewEndpoint("application-user-data-cm.s3.example.com", endpoint.RecordTypeCNAME, "doc-example-bucket.s3.amazonaws.com"). WithLabel(endpoint.ResourceLabelKey, "configmap/default/application-user-data-cm"), }, }, { title: "EndpointSlice for headless service with per-pod DNS", cfg: cfg{ resources: []string{"endpointslices.v1.discovery.k8s.io"}, fqdnTargetTemplate: ` {{if and (eq .Kind "EndpointSlice") (hasKey .Labels "service.kubernetes.io/headless")}} {{range $ep := .Object.endpoints}}{{if $ep.conditions.ready}}{{range $ep.addresses}}{{$ep.targetRef.name}}.pod.com:{{.}},{{end}}{{end}}{{end}}{{end}}`, }, objects: []*unstructured.Unstructured{ { Object: map[string]any{ "apiVersion": "discovery.k8s.io/v1", "kind": "EndpointSlice", "metadata": map[string]any{ "name": "test-headless-abc12", "namespace": "default", "labels": map[string]any{ "endpointslice.kubernetes.io/managed-by": "endpointslice-controller.k8s.io", "kubernetes.io/service-name": "test-headless", "service.kubernetes.io/headless": "", }, }, "addressType": "IPv4", "endpoints": []any{ map[string]any{ "addresses": []any{"10.244.1.2", "2001:db8::1"}, "conditions": map[string]any{ "ready": true, }, "nodeName": "worker1", "targetRef": map[string]any{ "kind": "Pod", "name": "app-abc12", "namespace": "default", }, }, map[string]any{ "addresses": []any{"10.244.2.3", "10.244.2.4"}, "conditions": map[string]any{ "ready": true, }, "nodeName": "worker2", "targetRef": map[string]any{ "kind": "Pod", "name": "app-def34", "namespace": "default", }, }, }, "ports": []any{ map[string]any{ "name": "http", "port": int64(80), "protocol": "TCP", }, }, }, }, }, expected: []*endpoint.Endpoint{ endpoint.NewEndpoint("app-abc12.pod.com", endpoint.RecordTypeA, "10.244.1.2"). WithLabel(endpoint.ResourceLabelKey, "endpointslice/default/test-headless-abc12"), endpoint.NewEndpoint("app-abc12.pod.com", endpoint.RecordTypeAAAA, "2001:db8::1"). WithLabel(endpoint.ResourceLabelKey, "endpointslice/default/test-headless-abc12"), endpoint.NewEndpoint("app-def34.pod.com", endpoint.RecordTypeA, "10.244.2.3", "10.244.2.4"). WithLabel(endpoint.ResourceLabelKey, "endpointslice/default/test-headless-abc12"), }, }, { title: "EndpointSlice for headless service with single FQDN per EndpointSlice", cfg: cfg{ resources: []string{"endpointslices.v1.discovery.k8s.io"}, fqdnTargetTemplate: ` {{if and (eq .Kind "EndpointSlice") (hasKey .Labels "service.kubernetes.io/headless")}} {{$svcName := index .Labels "kubernetes.io/service-name"}}{{range $ep := .Object.endpoints}} {{if $ep.conditions.ready}}{{range $ep.addresses}}{{$svcName}}.example.com:{{.}},{{end}}{{end}}{{end}}{{end}}`, }, objects: []*unstructured.Unstructured{ { Object: map[string]any{ "apiVersion": "discovery.k8s.io/v1", "kind": "EndpointSlice", "metadata": map[string]any{ "name": "test-headless-abc12", "namespace": "default", "labels": map[string]any{ "endpointslice.kubernetes.io/managed-by": "endpointslice-controller.k8s.io", "kubernetes.io/service-name": "my-headless", "service.kubernetes.io/headless": "", }, }, "addressType": "IPv4", "endpoints": []any{ map[string]any{ "addresses": []any{"10.244.1.2"}, "conditions": map[string]any{ "ready": true, }, "nodeName": "worker1", "targetRef": map[string]any{ "kind": "Pod", "name": "app-abc12", "namespace": "default", }, }, map[string]any{ "addresses": []any{"10.244.2.3", "10.244.2.4"}, "conditions": map[string]any{ "ready": true, }, "nodeName": "worker2", "targetRef": map[string]any{ "kind": "Pod", "name": "app-def34", "namespace": "default", }, }, }, "ports": []any{ map[string]any{ "name": "http", "port": int64(80), "protocol": "TCP", }, }, }, }, }, expected: []*endpoint.Endpoint{ endpoint.NewEndpoint("my-headless.example.com", endpoint.RecordTypeA, "10.244.1.2", "10.244.2.3", "10.244.2.4"). WithLabel(endpoint.ResourceLabelKey, "endpointslice/default/test-headless-abc12"), }, }, { title: "fqdnTargetTemplate returns no values when condition not met", cfg: cfg{ resources: []string{"endpointslices.v1.discovery.k8s.io"}, fqdnTargetTemplate: ` {{if and (eq .Kind "EndpointSlice") (hasKey .Labels "service.kubernetes.io/headless")}} {{range $ep := .Object.endpoints}}{{if $ep.conditions.ready}}{{range $ep.addresses}}{{$ep.targetRef.name}}.pod.com:{{.}},{{end}}{{end}}{{end}}{{end}}`, }, objects: []*unstructured.Unstructured{ { Object: map[string]any{ "apiVersion": "discovery.k8s.io/v1", "kind": "EndpointSlice", "metadata": map[string]any{ "name": "regular-service-abc12", "namespace": "default", "labels": map[string]any{ "endpointslice.kubernetes.io/managed-by": "endpointslice-controller.k8s.io", "kubernetes.io/service-name": "regular-service", // Note: missing service.kubernetes.io/headless label }, }, "addressType": "IPv4", "endpoints": []any{ map[string]any{ "addresses": []any{"10.244.1.2"}, "conditions": map[string]any{ "ready": true, }, "targetRef": map[string]any{ "kind": "Pod", "name": "app-abc12", "namespace": "default", }, }, }, }, }, }, expected: nil, }, { title: "both fqdnTargetTemplate and fqdnTemplate set - endpoints from both are combined", cfg: cfg{ resources: []string{"virtualmachineinstances.v1.kubevirt.io"}, fqdnTargetTemplate: `{{range $iface := .Status.interfaces}}{{$.Name}}-{{index $iface "name"}}.ifaces.example.com:{{index $iface "ipAddress"}},{{end}}`, fqdnTemplate: "{{.Name}}.vmi.example.com", targetTemplate: `{{index .Status.interfaces 0 "ipAddress"}}`, combine: true, }, objects: []*unstructured.Unstructured{ { Object: map[string]any{ "apiVersion": "kubevirt.io/v1", "kind": "VirtualMachineInstance", "metadata": map[string]any{ "name": "my-vm", "namespace": "default", }, "status": map[string]any{ "interfaces": []any{ map[string]any{ "name": "eth0", "ipAddress": "10.244.1.50", }, map[string]any{ "name": "eth1", "ipAddress": "192.168.1.50", }, }, }, }, }, }, expected: []*endpoint.Endpoint{ // from fqdnTargetTemplate: per-interface 1:1 host:IP pairs endpoint.NewEndpoint("my-vm-eth0.ifaces.example.com", endpoint.RecordTypeA, "10.244.1.50"). WithLabel(endpoint.ResourceLabelKey, "virtualmachineinstance/default/my-vm"), endpoint.NewEndpoint("my-vm-eth1.ifaces.example.com", endpoint.RecordTypeA, "192.168.1.50"). WithLabel(endpoint.ResourceLabelKey, "virtualmachineinstance/default/my-vm"), // from fqdnTemplate + targetTemplate: service-level record for the primary interface endpoint.NewEndpoint("my-vm.vmi.example.com", endpoint.RecordTypeA, "10.244.1.50"). WithLabel(endpoint.ResourceLabelKey, "virtualmachineinstance/default/my-vm"), }, }, { title: "fqdnTargetTemplate pair without colon separator is skipped", cfg: cfg{ resources: []string{"virtualmachineinstances.v1.kubevirt.io"}, fqdnTargetTemplate: "nocolonseparator", }, objects: []*unstructured.Unstructured{ { Object: map[string]any{ "apiVersion": "kubevirt.io/v1", "kind": "VirtualMachineInstance", "metadata": map[string]any{ "name": "my-vm", "namespace": "default", }, }, }, }, expected: nil, }, { title: "fqdnTargetTemplate pair with empty host is skipped", cfg: cfg{ resources: []string{"virtualmachineinstances.v1.kubevirt.io"}, fqdnTargetTemplate: ":10.0.0.1", }, objects: []*unstructured.Unstructured{ { Object: map[string]any{ "apiVersion": "kubevirt.io/v1", "kind": "VirtualMachineInstance", "metadata": map[string]any{ "name": "my-vm", "namespace": "default", }, }, }, }, expected: nil, }, { title: "fqdnTargetTemplate pair with empty target is skipped", cfg: cfg{ resources: []string{"virtualmachineinstances.v1.kubevirt.io"}, fqdnTargetTemplate: "host.example.com:", }, objects: []*unstructured.Unstructured{ { Object: map[string]any{ "apiVersion": "kubevirt.io/v1", "kind": "VirtualMachineInstance", "metadata": map[string]any{ "name": "my-vm", "namespace": "default", }, }, }, }, expected: nil, }, } { t.Run(tt.title, func(t *testing.T) { kubeClient, dynamicClient := setupUnstructuredTestClients(t, tt.cfg.resources, tt.objects) var selector labels.Selector if tt.cfg.labelFilter != "" { var err error selector, err = labels.Parse(tt.cfg.labelFilter) require.NoError(t, err) } else { selector = labels.Everything() } src, err := NewUnstructuredFQDNSource( t.Context(), dynamicClient, kubeClient, &Config{ LabelFilter: selector, UnstructuredResources: tt.cfg.resources, TemplateEngine: templatetest.MustEngine(t, tt.cfg.fqdnTemplate, tt.cfg.targetTemplate, tt.cfg.fqdnTargetTemplate, tt.cfg.combine), }, ) require.NoError(t, err) endpoints, err := src.Endpoints(t.Context()) require.NoError(t, err) testutils.ValidateEndpoints(t, endpoints, tt.expected) }) } } func TestUnstructuredWrapper_Templating(t *testing.T) { tests := []struct { name string tmpl string obj *unstructured.Unstructured want []string wantErr bool }{ { name: "typed-style Name and Namespace access", tmpl: "{{.Name}}.{{.Namespace}}.example.com", obj: &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "kubevirt.io/v1", "kind": "VirtualMachineInstance", "metadata": map[string]any{ "name": "my-vm", "namespace": "default", }, }, }, want: []string{"my-vm.default.example.com"}, }, { name: "raw Metadata map access", tmpl: "{{.Metadata.name}}.{{.Metadata.namespace}}.example.com", obj: &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "kubevirt.io/v1", "kind": "VirtualMachineInstance", "metadata": map[string]any{ "name": "my-vm", "namespace": "default", }, }, }, want: []string{"my-vm.default.example.com"}, }, { name: "nested Status field access", tmpl: "{{.Status.atProvider.endpoint.address}}", obj: &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "rds.aws.crossplane.io/v1alpha1", "kind": "RDSInstance", "metadata": map[string]any{ "name": "prod-db", "namespace": "default", }, "status": map[string]any{ "atProvider": map[string]any{ "endpoint": map[string]any{ "address": "prod-db.abc123.rds.amazonaws.com", }, }, }, }, }, want: []string{"prod-db.abc123.rds.amazonaws.com"}, }, { name: "array index access via Status", tmpl: `{{index .Status.interfaces 0 "ipAddress"}}`, obj: &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "kubevirt.io/v1", "kind": "VirtualMachineInstance", "metadata": map[string]any{ "name": "vm-1", "namespace": "default", }, "status": map[string]any{ "interfaces": []any{ map[string]any{ "ipAddress": "10.244.1.50", "name": "default", }, }, }, }, }, want: []string{"10.244.1.50"}, }, { name: "typed-style Labels access", tmpl: `{{index .Labels "app.kubernetes.io/instance"}}.example.com`, obj: &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "argoproj.io/v1alpha1", "kind": "Application", "metadata": map[string]any{ "name": "guestbook", "namespace": "argocd", "labels": map[string]any{ "app.kubernetes.io/instance": "guestbook-prod", }, }, }, }, want: []string{"guestbook-prod.example.com"}, }, { name: "Kind and APIVersion access", tmpl: "{{.Kind}}.{{.APIVersion}}.example.com", obj: &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "kubevirt.io/v1", "kind": "VirtualMachineInstance", "metadata": map[string]any{ "name": "test", "namespace": "default", }, }, }, want: []string{"VirtualMachineInstance.kubevirt.io/v1.example.com"}, }, { name: "Spec hosts array", tmpl: `{{index .Spec.hosts 0}}`, obj: &unstructured.Unstructured{ Object: map[string]any{ "apiVersion": "networking.istio.io/v1beta1", "kind": "VirtualService", "metadata": map[string]any{ "name": "reviews", "namespace": "bookinfo", }, "spec": map[string]any{ "hosts": []any{ "reviews.bookinfo.svc.cluster.local", }, }, }, }, want: []string{"reviews.bookinfo.svc.cluster.local"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { engine := templatetest.MustEngine(t, tt.tmpl, "", "", false) wrapped := newUnstructuredWrapper(tt.obj) got, err := engine.ExecFQDN(wrapped) if tt.wantErr { require.Error(t, err) return } require.NoError(t, err) assert.Equal(t, tt.want, got) }) } }