mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2026-04-15 21:11:01 +02:00
* test: improve code coverage Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> * test: improve code coverage Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com> --------- Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
1069 lines
33 KiB
Go
1069 lines
33 KiB
Go
/*
|
|
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)
|
|
})
|
|
}
|
|
}
|