From 7c4b53b7600815890a5800e524dc8a8f4a7e50e2 Mon Sep 17 00:00:00 2001 From: Ali Syed Date: Tue, 18 Aug 2026 19:09:13 +0100 Subject: [PATCH] OCPBUGS-58102: expose ExternalDNS operand metrics with native auth Expose ExternalDNS operand metrics to cluster Prometheus without using a kube-rbac-proxy sidecar. The operand now serves its own metrics over HTTPS with Kubernetes TokenReview/SAR auth via --metrics-tls-cert-dir (added in openshift/external-dns#200). - Add --metrics-tls-cert-dir arg and service-ca cert volume mount to each ExternalDNS container - Change --metrics-address from 127.0.0.1 to 0.0.0.0 so metrics are reachable outside the pod - Create a Service with serving-cert annotation for auto TLS - Create a ServiceMonitor for Prometheus discovery (HTTPS, port 7979+) - Add tokenreviews/subjectaccessreviews RBAC for the operand service account Depends on openshift/external-dns#200 for the operand-side auth layer. Assisted with Claude. --- ...al-dns-operator.clusterserviceversion.yaml | 16 + ...c.authorization.k8s.io_v1_clusterrole.yaml | 12 + config/manager/manager.yaml | 3 + config/rbac/operand_role.yaml | 12 + config/rbac/role.yaml | 13 + docs/openshift.md | 15 + .../controller/externaldns/controller.go | 18 + .../controller/externaldns/deployment.go | 48 +- .../controller/externaldns/deployment_test.go | 1066 +++++++++++++++-- pkg/operator/controller/externaldns/pod.go | 48 +- .../controller/externaldns/pod_test.go | 160 +++ .../controller/externaldns/service.go | 137 +++ .../controller/externaldns/service_test.go | 184 +++ .../controller/externaldns/servicemonitor.go | 154 +++ .../externaldns/servicemonitor_test.go | 206 ++++ pkg/operator/controller/names.go | 30 + pkg/operator/operator.go | 2 + 17 files changed, 2044 insertions(+), 80 deletions(-) create mode 100644 pkg/operator/controller/externaldns/service.go create mode 100644 pkg/operator/controller/externaldns/service_test.go create mode 100644 pkg/operator/controller/externaldns/servicemonitor.go create mode 100644 pkg/operator/controller/externaldns/servicemonitor_test.go diff --git a/bundle/manifests/external-dns-operator.clusterserviceversion.yaml b/bundle/manifests/external-dns-operator.clusterserviceversion.yaml index e9bbb496b..3ee973836 100644 --- a/bundle/manifests/external-dns-operator.clusterserviceversion.yaml +++ b/bundle/manifests/external-dns-operator.clusterserviceversion.yaml @@ -472,6 +472,7 @@ spec: - --operator-namespace=$(OPERATOR_NAMESPACE) - --operand-namespace=$(OPERATOR_NAMESPACE) - --externaldns-image=$(RELATED_IMAGE_EXTERNAL_DNS) + - --kube-rbac-proxy-image=$(RELATED_IMAGE_KUBE_RBAC_PROXY) - --trusted-ca-configmap=$(TRUSTED_CA_CONFIGMAP_NAME) - --leader-elect - --webhook-disable-http2 @@ -482,6 +483,8 @@ spec: fieldPath: metadata.namespace - name: RELATED_IMAGE_EXTERNAL_DNS value: quay.io/external-dns-operator/external-dns:latest + - name: RELATED_IMAGE_KUBE_RBAC_PROXY + value: quay.io/openshift/origin-kube-rbac-proxy:latest - name: TRUSTED_CA_CONFIGMAP_NAME image: quay.io/openshift/origin-external-dns-operator:latest name: external-dns-operator @@ -559,6 +562,7 @@ spec: - configmaps - secrets - serviceaccounts + - services verbs: - create - delete @@ -587,6 +591,18 @@ spec: - patch - update - watch + - apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - coordination.k8s.io resources: diff --git a/bundle/manifests/external-dns_rbac.authorization.k8s.io_v1_clusterrole.yaml b/bundle/manifests/external-dns_rbac.authorization.k8s.io_v1_clusterrole.yaml index b8d87de90..8a6e040f8 100644 --- a/bundle/manifests/external-dns_rbac.authorization.k8s.io_v1_clusterrole.yaml +++ b/bundle/manifests/external-dns_rbac.authorization.k8s.io_v1_clusterrole.yaml @@ -31,3 +31,15 @@ rules: - get - watch - list +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index e6f3c2189..ed434b2c9 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -36,6 +36,7 @@ spec: - --operator-namespace=$(OPERATOR_NAMESPACE) - --operand-namespace=$(OPERATOR_NAMESPACE) - --externaldns-image=$(RELATED_IMAGE_EXTERNAL_DNS) + - --kube-rbac-proxy-image=$(RELATED_IMAGE_KUBE_RBAC_PROXY) - --trusted-ca-configmap=$(TRUSTED_CA_CONFIGMAP_NAME) - --leader-elect - --webhook-disable-http2 @@ -49,6 +50,8 @@ spec: # Use "latest" floating tag to avoid problems with the prunning of older mirorred images. # Ref: https://issues.redhat.com/browse/OCPBUGS-57339. value: quay.io/external-dns-operator/external-dns:latest + - name: RELATED_IMAGE_KUBE_RBAC_PROXY + value: quay.io/openshift/origin-kube-rbac-proxy:latest - name: TRUSTED_CA_CONFIGMAP_NAME securityContext: capabilities: diff --git a/config/rbac/operand_role.yaml b/config/rbac/operand_role.yaml index 5022a8ec8..30da38b3a 100644 --- a/config/rbac/operand_role.yaml +++ b/config/rbac/operand_role.yaml @@ -31,3 +31,15 @@ rules: - get - watch - list + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index ab7f32884..c9d0407b2 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -73,6 +73,7 @@ rules: - configmaps - secrets - serviceaccounts + - services verbs: - create - delete @@ -101,3 +102,15 @@ rules: - patch - update - watch +- apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - create + - delete + - get + - list + - patch + - update + - watch diff --git a/docs/openshift.md b/docs/openshift.md index de59913fe..4e2bf3af8 100644 --- a/docs/openshift.md +++ b/docs/openshift.md @@ -10,3 +10,18 @@ Use the following convenience script to secure communication between the API and ```bash $ ./hack/add-serving-cert.sh --namespace external-dns-operator --service webhook-service --webhook validating-webhook-configuration --secret webhook-server-cert ``` + +## Operand metrics + +Each ExternalDNS operand deployment includes a [kube-rbac-proxy](https://github.com/brancz/kube-rbac-proxy) sidecar per zone container to expose metrics over HTTPS. A `Service` and `ServiceMonitor` are created per `ExternalDNS` CR for Prometheus discovery. + +### Multi-zone metric differentiation + +When an `ExternalDNS` instance manages multiple zones, one ExternalDNS container runs per zone — each exposing the same metric names. Metrics are kept separate (not combined or deduplicated) through the following mechanism: + +- Each zone container binds its metrics to a distinct localhost port (`:7979`, `:7980`, etc.). +- Each kube-rbac-proxy sidecar proxies one of those ports on a distinct secure port (`:9091`, `:9092`, etc.). +- The `ServiceMonitor` creates one endpoint entry per port. +- Prometheus assigns a unique `instance` label (`pod_ip:port`) to each scrape target. + +This means metrics from different zones within the same `ExternalDNS` instance are differentiated by port via the `instance` label. Metrics from different `ExternalDNS` instances are differentiated by pod or deployment name. diff --git a/pkg/operator/controller/externaldns/controller.go b/pkg/operator/controller/externaldns/controller.go index eb1043900..16ea24fad 100644 --- a/pkg/operator/controller/externaldns/controller.go +++ b/pkg/operator/controller/externaldns/controller.go @@ -25,6 +25,7 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -102,6 +103,16 @@ func New(mgr manager.Manager, cfg Config) (controller.Controller, error) { return nil, err } + if err := c.Watch(source.Kind[client.Object](operatorCache, &corev1.Service{}, handler.EnqueueRequestForOwner(operatorScheme, operatorRESTMapper, &operatorv1beta1.ExternalDNS{}, handler.OnlyControllerOwner()))); err != nil { + return nil, err + } + + smInformer := &unstructured.Unstructured{} + smInformer.SetGroupVersionKind(serviceMonitorGVK) + if err := c.Watch(source.Kind[client.Object](operatorCache, smInformer, handler.EnqueueRequestForOwner(operatorScheme, operatorRESTMapper, &operatorv1beta1.ExternalDNS{}, handler.OnlyControllerOwner()))); err != nil { + return nil, err + } + // secret replicated by the credentials controller // needs to trigger the reconciliation of the corresponding ExternalDNS // because of the annotation with the secret's hash in the operand deployment @@ -212,6 +223,13 @@ func (r *reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return reconcile.Result{}, fmt.Errorf("failed to ensure externalDNS deployment: %w", err) } + if err := r.ensureExternalDNSMetricsService(ctx, r.config.Namespace, externalDNS); err != nil { + return reconcile.Result{}, fmt.Errorf("failed to ensure externalDNS metrics service: %w", err) + } + if err := r.ensureExternalDNSServiceMonitor(ctx, r.config.Namespace, externalDNS); err != nil { + return reconcile.Result{}, fmt.Errorf("failed to ensure externalDNS service monitor: %w", err) + } + if err := r.updateExternalDNSStatus(ctx, externalDNS, currentDeployment, true); err != nil { return reconcile.Result{}, fmt.Errorf("failed to update externalDNS custom resource %s: %w", externalDNS.Name, err) } diff --git a/pkg/operator/controller/externaldns/deployment.go b/pkg/operator/controller/externaldns/deployment.go index 3d9c80cd9..2c52d89f7 100644 --- a/pkg/operator/controller/externaldns/deployment.go +++ b/pkg/operator/controller/externaldns/deployment.go @@ -109,16 +109,16 @@ func (r *reconciler) ensureExternalDNSDeployment(ctx context.Context, namespace, } desired, err := desiredExternalDNSDeployment(&deploymentConfig{ - namespace, - image, - serviceAccount, - externalDNS, - r.config.IsOpenShift, - r.config.PlatformStatus, - credSecret.Name, - credSecretHash, - trustCAConfigMapName, - trustCAConfigMapHash, + namespace: namespace, + image: image, + serviceAccount: serviceAccount, + externalDNS: externalDNS, + isOpenShift: r.config.IsOpenShift, + platformStatus: r.config.PlatformStatus, + secret: credSecret.Name, + secretHash: credSecretHash, + trustedCAConfigMapName: trustCAConfigMapName, + trustedCAConfigMapHash: trustCAConfigMapHash, }) if err != nil { return false, nil, fmt.Errorf("failed to build externalDNS deployment: %w", err) @@ -296,6 +296,9 @@ func desiredExternalDNSDeployment(cfg *deploymentConfig) (*appsv1.Deployment, er depl.Spec.Template.Spec.Containers = append(depl.Spec.Template.Spec.Containers, *container) } } + certVolume := metricsCertVolume(controller.ExternalDNSMetricsSecretName(cfg.externalDNS)) + depl.Spec.Template.Spec.Volumes = append(depl.Spec.Template.Spec.Volumes, certVolume) + return depl, nil } @@ -418,6 +421,10 @@ func externalDNSContainersChanged(current, expected, updated *appsv1.Deployment) updated.Spec.Template.Spec.Containers[currCont.Index].SecurityContext = updatedContext changed = true } + if !equalContainerPorts(currCont.Ports, expCont.Ports) { + updated.Spec.Template.Spec.Containers[currCont.Index].Ports = expCont.Ports + changed = true + } } else { // expected container is not present - add it updated.Spec.Template.Spec.Containers = append(updated.Spec.Template.Spec.Containers, expCont.Container) @@ -701,6 +708,27 @@ func securityContextChanged(current, updated, desired *corev1.SecurityContext) ( return changed, updated } +// equalContainerPorts returns true if 2 container port slices have the same content. +func equalContainerPorts(current, expected []corev1.ContainerPort) bool { + if len(current) != len(expected) { + return false + } + currentMap := map[string]corev1.ContainerPort{} + for _, p := range current { + currentMap[p.Name] = p + } + for _, ep := range expected { + cp, found := currentMap[ep.Name] + if !found { + return false + } + if cp.ContainerPort != ep.ContainerPort || cp.Protocol != ep.Protocol { + return false + } + } + return true +} + func equalBoolPtr(current, desired *bool) bool { if desired == nil { return true diff --git a/pkg/operator/controller/externaldns/deployment_test.go b/pkg/operator/controller/externaldns/deployment_test.go index e34c0493b..f734d779b 100644 --- a/pkg/operator/controller/externaldns/deployment_test.go +++ b/pkg/operator/controller/externaldns/deployment_test.go @@ -156,13 +156,22 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -186,6 +195,11 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: awsCredentialsVolumeName, MountPath: awsCredentialsMountPath, @@ -250,12 +264,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -272,6 +297,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--fqdn-template={{.Name}}.test.com", "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -343,13 +375,22 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -373,6 +414,11 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: "trusted-ca", ReadOnly: true, @@ -465,13 +511,22 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -500,6 +555,11 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: awsCredentialsVolumeName, MountPath: awsCredentialsMountPath, @@ -580,13 +640,22 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=azure", @@ -606,6 +675,11 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--azure-config-file=/etc/kubernetes/azure.json", }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: azureConfigVolumeName, ReadOnly: true, @@ -681,13 +755,22 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: "external-dns-n64ch5cch658h64bq", Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=/subscriptions/xxxx/resourceGroups/test-az-2f9kj-rg/providers/Microsoft.Network/privateDnsZones/test-az.example.com", "--provider=azure-private-dns", @@ -707,6 +790,11 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--txt-wildcard-replacement=any", }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: azureConfigVolumeName, ReadOnly: true, @@ -766,12 +854,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=azure", @@ -789,6 +888,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--txt-prefix=external-dns-", "--txt-wildcard-replacement=any", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -858,13 +964,22 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerNoZones, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--provider=azure", "--source=service", @@ -883,6 +998,11 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--azure-config-file=/etc/kubernetes/azure.json", }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: azureConfigVolumeName, ReadOnly: true, @@ -905,7 +1025,8 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Name: ExternalDNSContainerNoZones, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7980", + "--metrics-address=0.0.0.0:7980", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--provider=azure-private-dns", "--source=service", @@ -924,6 +1045,11 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--azure-config-file=/etc/kubernetes/azure.json", }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: azureConfigVolumeName, ReadOnly: true, @@ -999,13 +1125,22 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=google", @@ -1030,6 +1165,11 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: gcpCredentialsVolumeName, ReadOnly: true, @@ -1089,12 +1229,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=google", @@ -1111,6 +1262,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--fqdn-template={{.Name}}.test.com", "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -1166,12 +1324,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=google", @@ -1189,6 +1358,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--txt-prefix=external-dns-", "--google-project=external-dns-gcp-project", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -1258,13 +1434,22 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=bluecat", @@ -1283,6 +1468,11 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--bluecat-config-file=/etc/kubernetes/bluecat.json", }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: "bluecat-config-file", ReadOnly: true, @@ -1342,12 +1532,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=bluecat", @@ -1364,6 +1565,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--txt-prefix=external-dns-", "--fqdn-template={{.Name}}.test.com", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -1418,12 +1626,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=infoblox", @@ -1467,6 +1686,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -1521,12 +1747,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=infoblox", @@ -1572,6 +1809,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -1625,12 +1869,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=infoblox", @@ -1646,6 +1901,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--ignore-hostname-annotation", "--fqdn-template={{.Name}}.test.com", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -1699,12 +1961,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -1715,6 +1988,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--service-type-filter=LoadBalancer", "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -1768,12 +2048,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -1786,6 +2077,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--fqdn-template={{.Name}}.test.com,{{.Name}}.{{.Namespace}}.example.com", "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -1839,12 +2137,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -1861,6 +2170,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--fqdn-template={{.Name}}.test.com", "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -1877,7 +2193,8 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Name: "external-dns-n656hcdh5d9hf6q", Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7980", + "--metrics-address=0.0.0.0:7980", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-private-zone", "--provider=aws", @@ -1894,6 +2211,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--fqdn-template={{.Name}}.test.com", "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -1947,12 +2271,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -1970,6 +2305,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--fqdn-template={{.Name}}.test.com", "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -2023,12 +2365,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerNoZones, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--provider=aws", "--source=service", @@ -2044,6 +2397,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--fqdn-template={{.Name}}.test.com", "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -2097,13 +2457,24 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerNoZones, Image: test.OperandImage, Args: []string{ "--domain-filter=abc.com", - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--provider=aws", "--source=service", @@ -2119,6 +2490,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--fqdn-template={{.Name}}.test.com", "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -2172,6 +2550,16 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, @@ -2179,7 +2567,8 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Args: []string{ "--domain-filter=abc.com", "--zone-id-filter=my-dns-public-zone", - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--provider=aws", "--source=service", @@ -2195,6 +2584,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--fqdn-template={{.Name}}.test.com", "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -2281,13 +2677,22 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -2306,6 +2711,11 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: awsCredentialsVolumeName, MountPath: awsCredentialsMountPath, @@ -2370,12 +2780,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -2387,6 +2808,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { `--fqdn-template={{""}}`, "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -2472,13 +2900,22 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -2497,6 +2934,11 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: awsCredentialsVolumeName, MountPath: awsCredentialsMountPath, @@ -2561,13 +3003,24 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ "--aws-assume-role=arn:aws:iam:123456789012:role/foo", - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -2579,6 +3032,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { `--fqdn-template={{""}}`, "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -2648,13 +3108,22 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=azure", @@ -2669,6 +3138,11 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--txt-wildcard-replacement=any", }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: azureConfigVolumeName, ReadOnly: true, @@ -2728,12 +3202,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=azure", @@ -2746,6 +3231,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--txt-prefix=external-dns-", "--txt-wildcard-replacement=any", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -2815,13 +3307,22 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerNoZones, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--provider=azure", "--source=openshift-route", @@ -2835,6 +3336,11 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--txt-wildcard-replacement=any", }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: azureConfigVolumeName, ReadOnly: true, @@ -2857,7 +3363,8 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Name: ExternalDNSContainerNoZones, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7980", + "--metrics-address=0.0.0.0:7980", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--provider=azure-private-dns", "--source=openshift-route", @@ -2871,6 +3378,11 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--txt-wildcard-replacement=any", }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: azureConfigVolumeName, ReadOnly: true, @@ -2946,13 +3458,22 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=google", @@ -2972,6 +3493,11 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: gcpCredentialsVolumeName, ReadOnly: true, @@ -3031,12 +3557,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=google", @@ -3048,6 +3585,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { `--fqdn-template={{""}}`, "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -3117,13 +3661,22 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=bluecat", @@ -3137,6 +3690,11 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--txt-prefix=external-dns-", }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: "bluecat-config-file", ReadOnly: true, @@ -3196,12 +3754,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=bluecat", @@ -3213,6 +3782,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { `--fqdn-template={{""}}`, "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -3267,12 +3843,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=infoblox", @@ -3311,6 +3898,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { }, }, }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -3364,12 +3958,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=infoblox", @@ -3380,6 +3985,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--ignore-hostname-annotation", `--fqdn-template={{""}}`, }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -3433,12 +4045,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -3448,6 +4071,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { "--log-level=debug", "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -3501,12 +4131,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -3518,6 +4159,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { `--fqdn-template={{""}}`, "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -3534,7 +4182,8 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Name: "external-dns-n656hcdh5d9hf6q", Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7980", + "--metrics-address=0.0.0.0:7980", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-private-zone", "--provider=aws", @@ -3546,6 +4195,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { `--fqdn-template={{""}}`, "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -3599,12 +4255,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -3617,6 +4284,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { `--fqdn-template={{""}}`, "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -3670,12 +4344,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerNoZones, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--provider=aws", "--source=openshift-route", @@ -3686,6 +4371,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { `--fqdn-template={{""}}`, "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -3739,13 +4431,24 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerNoZones, Image: test.OperandImage, Args: []string{ "--domain-filter=abc.com", - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--provider=aws", "--source=openshift-route", @@ -3756,6 +4459,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { `--fqdn-template={{""}}`, "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -3809,6 +4519,16 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, @@ -3816,7 +4536,8 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Args: []string{ "--domain-filter=abc.com", "--zone-id-filter=my-dns-public-zone", - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--provider=aws", "--source=openshift-route", @@ -3827,6 +4548,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { `--fqdn-template={{""}}`, "--txt-prefix=external-dns-", }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -3885,12 +4613,23 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Effect: corev1.TaintEffectNoSchedule, }, }, + Volumes: []corev1.Volume{ + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, + }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -3916,6 +4655,13 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { Value: noProxy, }, }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, + }, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{allCapabilities}, @@ -3950,15 +4696,15 @@ func TestDesiredExternalDNSDeployment(t *testing.T) { } }() depl, err := desiredExternalDNSDeployment(&deploymentConfig{ - test.OperandNamespace, - test.OperandImage, - serviceAccount, - tc.inputExternalDNS, - tc.inputIsOpenShift, - tc.inputPlatformStatus, - tc.inputSecretName, - testSecretHash, - tc.inputTrustedCAConfigMapName, "", + namespace: test.OperandNamespace, + image: test.OperandImage, + serviceAccount: serviceAccount, + externalDNS: tc.inputExternalDNS, + isOpenShift: tc.inputIsOpenShift, + platformStatus: tc.inputPlatformStatus, + secret: tc.inputSecretName, + secretHash: testSecretHash, + trustedCAConfigMapName: tc.inputTrustedCAConfigMapName, }) if err != nil { t.Errorf("expected no error from calling desiredExternalDNSDeployment, but received %v", err) @@ -4324,13 +5070,22 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -4347,6 +5102,11 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: awsCredentialsVolumeName, MountPath: awsCredentialsMountPath, @@ -4453,13 +5213,22 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -4476,6 +5245,11 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: awsCredentialsVolumeName, MountPath: awsCredentialsMountPath, @@ -4583,13 +5357,22 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -4606,6 +5389,11 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: awsCredentialsVolumeName, MountPath: awsCredentialsMountPath, @@ -4712,13 +5500,22 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -4734,6 +5531,11 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: awsCredentialsVolumeName, MountPath: awsCredentialsMountPath, @@ -4842,13 +5644,22 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -4866,6 +5677,11 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: awsCredentialsVolumeName, MountPath: awsCredentialsMountPath, @@ -5030,13 +5846,22 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -5053,6 +5878,11 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: awsCredentialsVolumeName, MountPath: awsCredentialsMountPath, @@ -5184,13 +6014,22 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -5211,6 +6050,11 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: "trusted-ca", MountPath: "/etc/pki/ca-trust/extracted/pem", @@ -5327,13 +6171,22 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -5351,6 +6204,11 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: awsCredentialsVolumeName, MountPath: awsCredentialsMountPath, @@ -5457,13 +6315,22 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -5481,6 +6348,11 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: awsCredentialsVolumeName, MountPath: awsCredentialsMountPath, @@ -5577,13 +6449,22 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -5601,6 +6482,11 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: awsCredentialsVolumeName, MountPath: awsCredentialsMountPath, @@ -5768,13 +6654,22 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -5792,6 +6687,11 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: awsCredentialsVolumeName, MountPath: awsCredentialsMountPath, @@ -5897,13 +6797,22 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, }, Containers: []corev1.Container{ { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -5925,6 +6834,11 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: awsCredentialsVolumeName, MountPath: "wrongpath", @@ -6019,6 +6933,14 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, }, + { + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "external-dns-test-metrics-cert", + }, + }, + }, { Name: "bound-sa-token", VolumeSource: corev1.VolumeSource{ @@ -6041,7 +6963,8 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { Name: ExternalDNSContainerName, Image: test.OperandImage, Args: []string{ - "--metrics-address=127.0.0.1:7979", + "--metrics-address=0.0.0.0:7979", + "--metrics-tls-cert-dir=/var/run/secrets/serving-cert", "--txt-owner-id=external-dns-test", "--zone-id-filter=my-dns-public-zone", "--provider=aws", @@ -6059,6 +6982,11 @@ func TestEnsureExternalDNSDeployment(t *testing.T) { }, }, VolumeMounts: []corev1.VolumeMount{ + { + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }, { Name: awsCredentialsVolumeName, MountPath: awsCredentialsMountPath, diff --git a/pkg/operator/controller/externaldns/pod.go b/pkg/operator/controller/externaldns/pod.go index daf004c4e..5abe390fc 100644 --- a/pkg/operator/controller/externaldns/pod.go +++ b/pkg/operator/controller/externaldns/pod.go @@ -35,7 +35,7 @@ import ( ) const ( - defaultMetricsAddress = "127.0.0.1" + defaultMetricsAddress = "0.0.0.0" defaultOwnerPrefix = "external-dns" defaultMetricsStartPort = 7979 defaultConfigMountPath = "/etc/kubernetes" @@ -57,6 +57,12 @@ const ( // all capabilities in the container security context allCapabilities = "ALL" // + // metrics TLS serving cert (provisioned by service-ca) + // + metricsCertVolumeName = "metrics-cert" + metricsCertMountPath = "/var/run/secrets/serving-cert" + metricsPortName = "metrics" + // // AWS // awsCredentialEnvVarName = "AWS_SHARED_CREDENTIALS_FILE" @@ -163,6 +169,7 @@ func (b *externalDNSContainerBuilder) fillProviderAgnosticFields(seq int, zone s // args := []string{ fmt.Sprintf("--metrics-address=%s:%d", defaultMetricsAddress, defaultMetricsStartPort+seq), + fmt.Sprintf("--metrics-tls-cert-dir=%s", metricsCertMountPath), fmt.Sprintf("--txt-owner-id=%s-%s", defaultOwnerPrefix, b.externalDNS.Name), fmt.Sprintf("--provider=%s", b.provider), fmt.Sprintf("--source=%s", b.source), @@ -245,6 +252,12 @@ func (b *externalDNSContainerBuilder) fillProviderAgnosticFields(seq int, zone s // // VOLUME MOUNTS // + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + Name: metricsCertVolumeName, + MountPath: metricsCertMountPath, + ReadOnly: true, + }) + for _, v := range b.volumes { // if trustedCA volume was added if v.Name == trustedCAVolumeName { @@ -694,3 +707,36 @@ func (b *externalDNSVolumeBuilder) bluecatVolumes() []corev1.Volume { func addTXTPrefixFlag(args []string) []string { return append(args, fmt.Sprintf("--txt-prefix=%s", defaultTXTRecordPrefix)) } + +// numMetricsPorts returns the number of metrics ports needed for the given ExternalDNS instance. +// This mirrors the zone container creation logic in desiredExternalDNSDeployment. +func numMetricsPorts(externalDNS *operatorv1beta1.ExternalDNS) int { + if len(externalDNS.Spec.Zones) == 0 { + if externalDNS.Spec.Provider.Type == operatorv1beta1.ProviderTypeAzure { + return 2 + } + return 1 + } + return len(externalDNS.Spec.Zones) +} + +// metricsPortNameForSeq returns the port name for the ExternalDNS container metrics port +// at the given sequence index. +func metricsPortNameForSeq(seq int) string { + if seq == 0 { + return metricsPortName + } + return fmt.Sprintf("%s-%d", metricsPortName, seq) +} + +// metricsCertVolume returns the volume for the metrics serving certificate secret. +func metricsCertVolume(secretName string) corev1.Volume { + return corev1.Volume{ + Name: metricsCertVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secretName, + }, + }, + } +} diff --git a/pkg/operator/controller/externaldns/pod_test.go b/pkg/operator/controller/externaldns/pod_test.go index 82bfafc2a..5cbde0055 100644 --- a/pkg/operator/controller/externaldns/pod_test.go +++ b/pkg/operator/controller/externaldns/pod_test.go @@ -1,10 +1,13 @@ package externaldnscontroller import ( + "fmt" "reflect" "strings" "testing" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" "github.com/openshift/external-dns-operator/api/v1beta1" @@ -260,3 +263,160 @@ func TestDomainFilters(t *testing.T) { }) } } + +func TestNumMetricsPorts(t *testing.T) { + testCases := []struct { + name string + extDNS *v1beta1.ExternalDNS + expected int + }{ + { + name: "no zones, non-Azure provider", + extDNS: &v1beta1.ExternalDNS{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: v1beta1.ExternalDNSSpec{ + Provider: v1beta1.ExternalDNSProvider{Type: v1beta1.ProviderTypeAWS}, + }, + }, + expected: 1, + }, + { + name: "no zones, Azure provider", + extDNS: &v1beta1.ExternalDNS{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: v1beta1.ExternalDNSSpec{ + Provider: v1beta1.ExternalDNSProvider{Type: v1beta1.ProviderTypeAzure}, + }, + }, + expected: 2, + }, + { + name: "3 zones", + extDNS: &v1beta1.ExternalDNS{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: v1beta1.ExternalDNSSpec{ + Provider: v1beta1.ExternalDNSProvider{Type: v1beta1.ProviderTypeAWS}, + Zones: []string{"zone1", "zone2", "zone3"}, + }, + }, + expected: 3, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := numMetricsPorts(tc.extDNS) + if got != tc.expected { + t.Errorf("expected %d, got %d", tc.expected, got) + } + }) + } +} + +func TestMetricsPortNameForSeq(t *testing.T) { + testCases := []struct { + seq int + expected string + }{ + {0, "metrics"}, + {1, "metrics-1"}, + {2, "metrics-2"}, + } + for _, tc := range testCases { + t.Run(fmt.Sprintf("seq=%d", tc.seq), func(t *testing.T) { + got := metricsPortNameForSeq(tc.seq) + if got != tc.expected { + t.Errorf("expected %q, got %q", tc.expected, got) + } + }) + } +} + +func TestMetricsCertVolume(t *testing.T) { + secretName := "test-metrics-cert" + vol := metricsCertVolume(secretName) + + if vol.Name != metricsCertVolumeName { + t.Errorf("expected volume name %q, got %q", metricsCertVolumeName, vol.Name) + } + if vol.Secret == nil { + t.Fatal("expected secret volume source") + } + if vol.Secret.SecretName != secretName { + t.Errorf("expected secret name %q, got %q", secretName, vol.Secret.SecretName) + } +} + +func TestEqualContainerPorts(t *testing.T) { + testCases := []struct { + name string + current []corev1.ContainerPort + expected []corev1.ContainerPort + equal bool + }{ + { + name: "both empty", + current: nil, + expected: nil, + equal: true, + }, + { + name: "identical", + current: []corev1.ContainerPort{ + {Name: "https", ContainerPort: 8443, Protocol: corev1.ProtocolTCP}, + }, + expected: []corev1.ContainerPort{ + {Name: "https", ContainerPort: 8443, Protocol: corev1.ProtocolTCP}, + }, + equal: true, + }, + { + name: "different length", + current: []corev1.ContainerPort{ + {Name: "https", ContainerPort: 8443, Protocol: corev1.ProtocolTCP}, + }, + expected: []corev1.ContainerPort{ + {Name: "https", ContainerPort: 8443, Protocol: corev1.ProtocolTCP}, + {Name: "https-1", ContainerPort: 8444, Protocol: corev1.ProtocolTCP}, + }, + equal: false, + }, + { + name: "different port number", + current: []corev1.ContainerPort{ + {Name: "https", ContainerPort: 8443, Protocol: corev1.ProtocolTCP}, + }, + expected: []corev1.ContainerPort{ + {Name: "https", ContainerPort: 9999, Protocol: corev1.ProtocolTCP}, + }, + equal: false, + }, + { + name: "different protocol", + current: []corev1.ContainerPort{ + {Name: "https", ContainerPort: 8443, Protocol: corev1.ProtocolTCP}, + }, + expected: []corev1.ContainerPort{ + {Name: "https", ContainerPort: 8443, Protocol: corev1.ProtocolUDP}, + }, + equal: false, + }, + { + name: "missing port name", + current: []corev1.ContainerPort{ + {Name: "other", ContainerPort: 8443, Protocol: corev1.ProtocolTCP}, + }, + expected: []corev1.ContainerPort{ + {Name: "https", ContainerPort: 8443, Protocol: corev1.ProtocolTCP}, + }, + equal: false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := equalContainerPorts(tc.current, tc.expected) + if got != tc.equal { + t.Errorf("expected %v, got %v", tc.equal, got) + } + }) + } +} diff --git a/pkg/operator/controller/externaldns/service.go b/pkg/operator/controller/externaldns/service.go new file mode 100644 index 000000000..6719e525a --- /dev/null +++ b/pkg/operator/controller/externaldns/service.go @@ -0,0 +1,137 @@ +/* +Copyright 2026. + +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 externaldnscontroller + +import ( + "context" + "fmt" + "reflect" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + operatorv1beta1 "github.com/openshift/external-dns-operator/api/v1beta1" + controller "github.com/openshift/external-dns-operator/pkg/operator/controller" +) + +// desiredMetricsService returns the desired metrics Service for the given ExternalDNS instance. +// It creates one port per zone container, matching the ExternalDNS container metrics ports directly. +func desiredMetricsService(namespace string, externalDNS *operatorv1beta1.ExternalDNS) *corev1.Service { + metricsSecretName := controller.ExternalDNSMetricsSecretName(externalDNS) + numZones := numMetricsPorts(externalDNS) + + ports := make([]corev1.ServicePort, numZones) + for i := 0; i < numZones; i++ { + portName := metricsPortNameForSeq(i) + ports[i] = corev1.ServicePort{ + Name: portName, + Port: int32(defaultMetricsStartPort + i), + TargetPort: intstr.FromString(portName), + Protocol: corev1.ProtocolTCP, + } + } + + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: controller.ExternalDNSMetricsServiceName(externalDNS), + Namespace: namespace, + Labels: map[string]string{ + appNameLabel: controller.ExternalDNSBaseName, + appInstanceLabel: externalDNS.Name, + }, + Annotations: map[string]string{ + "service.beta.openshift.io/serving-cert-secret-name": metricsSecretName, + }, + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + appNameLabel: controller.ExternalDNSBaseName, + appInstanceLabel: externalDNS.Name, + }, + Ports: ports, + }, + } +} + +// ensureExternalDNSMetricsService ensures that the metrics service for the operand exists. +func (r *reconciler) ensureExternalDNSMetricsService(ctx context.Context, namespace string, externalDNS *operatorv1beta1.ExternalDNS) error { + desired := desiredMetricsService(namespace, externalDNS) + + if err := controllerutil.SetControllerReference(externalDNS, desired, r.scheme); err != nil { + return fmt.Errorf("failed to set the controller reference for metrics service: %w", err) + } + + current := &corev1.Service{} + nsName := types.NamespacedName{Namespace: desired.Namespace, Name: desired.Name} + err := r.client.Get(ctx, nsName, current) + if err != nil { + if !errors.IsNotFound(err) { + return fmt.Errorf("failed to get metrics service %s: %w", nsName, err) + } + if err := r.client.Create(ctx, desired); err != nil { + return fmt.Errorf("failed to create metrics service %s/%s: %w", desired.Namespace, desired.Name, err) + } + r.log.Info("created metrics service", "namespace", desired.Namespace, "name", desired.Name) + return nil + } + + if metricsServiceChanged(current, desired) { + desired.Spec.ClusterIP = current.Spec.ClusterIP + desired.Spec.ClusterIPs = current.Spec.ClusterIPs + desired.Spec.IPFamilies = current.Spec.IPFamilies + desired.Spec.IPFamilyPolicy = current.Spec.IPFamilyPolicy + desired.ResourceVersion = current.ResourceVersion + if err := r.client.Update(ctx, desired); err != nil { + return fmt.Errorf("failed to update metrics service %s/%s: %w", desired.Namespace, desired.Name, err) + } + r.log.Info("updated metrics service", "namespace", desired.Namespace, "name", desired.Name) + } + + return nil +} + +// metricsServiceChanged returns true if the current service needs to be updated to match the desired. +func metricsServiceChanged(current, desired *corev1.Service) bool { + for k, v := range desired.Labels { + if current.Labels[k] != v { + return true + } + } + for k, v := range desired.Annotations { + if current.Annotations[k] != v { + return true + } + } + if !reflect.DeepEqual(current.Spec.Selector, desired.Spec.Selector) { + return true + } + if len(current.Spec.Ports) != len(desired.Spec.Ports) { + return true + } + for i := range desired.Spec.Ports { + if current.Spec.Ports[i].Name != desired.Spec.Ports[i].Name || + current.Spec.Ports[i].Port != desired.Spec.Ports[i].Port || + current.Spec.Ports[i].TargetPort != desired.Spec.Ports[i].TargetPort { + return true + } + } + return false +} diff --git a/pkg/operator/controller/externaldns/service_test.go b/pkg/operator/controller/externaldns/service_test.go new file mode 100644 index 000000000..7f3fa65d0 --- /dev/null +++ b/pkg/operator/controller/externaldns/service_test.go @@ -0,0 +1,184 @@ +/* +Copyright 2026. + +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 externaldnscontroller + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + operatorv1beta1 "github.com/openshift/external-dns-operator/api/v1beta1" + controller "github.com/openshift/external-dns-operator/pkg/operator/controller" + "github.com/openshift/external-dns-operator/pkg/operator/controller/utils/test" +) + +func TestMetricsService(t *testing.T) { + testCases := []struct { + name string + externalDNS *operatorv1beta1.ExternalDNS + namespace string + expectedPorts int + expectedName string + }{ + { + name: "single zone AWS", + externalDNS: testAWSExternalDNS(operatorv1beta1.SourceTypeService), + namespace: test.OperandNamespace, + expectedPorts: 1, + expectedName: controller.ExternalDNSMetricsServiceName(testAWSExternalDNS(operatorv1beta1.SourceTypeService)), + }, + { + name: "multiple zones AWS", + externalDNS: testAWSExternalDNSZones([]string{test.PublicZone, test.PrivateZone}, operatorv1beta1.SourceTypeService), + namespace: test.OperandNamespace, + expectedPorts: 2, + expectedName: controller.ExternalDNSMetricsServiceName(testAWSExternalDNSZones([]string{test.PublicZone, test.PrivateZone}, operatorv1beta1.SourceTypeService)), + }, + { + name: "Azure no zones creates 2 ports", + externalDNS: testAzureExternalDNSNoZones(operatorv1beta1.SourceTypeService), + namespace: test.OperandNamespace, + expectedPorts: 2, + expectedName: controller.ExternalDNSMetricsServiceName(testAzureExternalDNSNoZones(operatorv1beta1.SourceTypeService)), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + svc := desiredMetricsService(tc.namespace, tc.externalDNS) + + if svc.Name != tc.expectedName { + t.Errorf("expected service name %q, got %q", tc.expectedName, svc.Name) + } + if svc.Namespace != tc.namespace { + t.Errorf("expected namespace %q, got %q", tc.namespace, svc.Namespace) + } + if len(svc.Spec.Ports) != tc.expectedPorts { + t.Errorf("expected %d ports, got %d", tc.expectedPorts, len(svc.Spec.Ports)) + } + + // Verify serving cert annotation. + expectedSecretName := controller.ExternalDNSMetricsSecretName(tc.externalDNS) + if svc.Annotations["service.beta.openshift.io/serving-cert-secret-name"] != expectedSecretName { + t.Errorf("expected serving cert annotation %q, got %q", expectedSecretName, svc.Annotations["service.beta.openshift.io/serving-cert-secret-name"]) + } + + // Verify labels match selector. + if svc.Labels[appNameLabel] != controller.ExternalDNSBaseName { + t.Errorf("expected label %s=%s, got %s", appNameLabel, controller.ExternalDNSBaseName, svc.Labels[appNameLabel]) + } + + // Verify port names and numbering. + for i, port := range svc.Spec.Ports { + expectedPortName := metricsPortNameForSeq(i) + if port.Name != expectedPortName { + t.Errorf("port %d: expected name %q, got %q", i, expectedPortName, port.Name) + } + expectedPort := int32(defaultMetricsStartPort + i) + if port.Port != expectedPort { + t.Errorf("port %d: expected port %d, got %d", i, expectedPort, port.Port) + } + if port.TargetPort != intstr.FromString(expectedPortName) { + t.Errorf("port %d: expected target port %q, got %v", i, expectedPortName, port.TargetPort) + } + } + }) + } +} + +func TestMetricsServiceChanged(t *testing.T) { + extDNS := testAWSExternalDNS(operatorv1beta1.SourceTypeService) + base := desiredMetricsService(test.OperandNamespace, extDNS) + + testCases := []struct { + name string + mutate func(*corev1.Service) + changed bool + }{ + { + name: "no change", + mutate: func(s *corev1.Service) {}, + changed: false, + }, + { + name: "label changed", + mutate: func(s *corev1.Service) { + s.Labels[appInstanceLabel] = "different" + }, + changed: true, + }, + { + name: "annotation changed", + mutate: func(s *corev1.Service) { + s.Annotations["service.beta.openshift.io/serving-cert-secret-name"] = "wrong-secret" + }, + changed: true, + }, + { + name: "selector changed", + mutate: func(s *corev1.Service) { + s.Spec.Selector[appInstanceLabel] = "different" + }, + changed: true, + }, + { + name: "port count changed", + mutate: func(s *corev1.Service) { + s.Spec.Ports = append(s.Spec.Ports, corev1.ServicePort{ + Name: "extra", + Port: 9999, + }) + }, + changed: true, + }, + { + name: "port name changed", + mutate: func(s *corev1.Service) { + s.Spec.Ports[0].Name = "changed" + }, + changed: true, + }, + { + name: "port number changed", + mutate: func(s *corev1.Service) { + s.Spec.Ports[0].Port = 9999 + }, + changed: true, + }, + { + name: "target port changed", + mutate: func(s *corev1.Service) { + s.Spec.Ports[0].TargetPort = intstr.FromInt(1234) + }, + changed: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + current := base.DeepCopy() + tc.mutate(current) + desired := desiredMetricsService(test.OperandNamespace, extDNS) + + got := metricsServiceChanged(current, desired) + if got != tc.changed { + t.Errorf("expected changed=%v, got %v", tc.changed, got) + } + }) + } +} diff --git a/pkg/operator/controller/externaldns/servicemonitor.go b/pkg/operator/controller/externaldns/servicemonitor.go new file mode 100644 index 000000000..829cc06f5 --- /dev/null +++ b/pkg/operator/controller/externaldns/servicemonitor.go @@ -0,0 +1,154 @@ +/* +Copyright 2026. + +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 externaldnscontroller + +import ( + "context" + "fmt" + "reflect" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + operatorv1beta1 "github.com/openshift/external-dns-operator/api/v1beta1" + controller "github.com/openshift/external-dns-operator/pkg/operator/controller" +) + +var serviceMonitorGVK = schema.GroupVersionKind{ + Group: "monitoring.coreos.com", + Version: "v1", + Kind: "ServiceMonitor", +} + +// ensureExternalDNSServiceMonitor ensures that the service monitor for the operand exists. +func (r *reconciler) ensureExternalDNSServiceMonitor(ctx context.Context, namespace string, externalDNS *operatorv1beta1.ExternalDNS) error { + desired := desiredServiceMonitor(namespace, externalDNS) + + if err := controllerutil.SetControllerReference(externalDNS, desired, r.scheme); err != nil { + return fmt.Errorf("failed to set the controller reference for service monitor: %w", err) + } + + current := &unstructured.Unstructured{} + current.SetGroupVersionKind(serviceMonitorGVK) + nsName := types.NamespacedName{Namespace: namespace, Name: controller.ExternalDNSServiceMonitorName(externalDNS)} + + err := r.client.Get(ctx, nsName, current) + if err != nil { + if !errors.IsNotFound(err) { + return fmt.Errorf("failed to get service monitor %s: %w", nsName, err) + } + if err := r.client.Create(ctx, desired); err != nil { + return fmt.Errorf("failed to create service monitor %s/%s: %w", namespace, desired.GetName(), err) + } + r.log.Info("created service monitor", "namespace", namespace, "name", desired.GetName()) + return nil + } + + // Update the spec if the fields we manage have drifted. + // We compare only the fields we set (endpoints, selector, namespaceSelector) + // rather than the full spec, because the API server may add defaulted fields + // that would cause reflect.DeepEqual to always detect drift. + if serviceMonitorChanged(current, desired) { + desiredSpec, _, _ := unstructured.NestedMap(desired.Object, "spec") + current.Object["spec"] = desiredSpec + current.SetLabels(desired.GetLabels()) + if err := r.client.Update(ctx, current); err != nil { + return fmt.Errorf("failed to update service monitor %s/%s: %w", namespace, current.GetName(), err) + } + r.log.Info("updated service monitor", "namespace", namespace, "name", current.GetName()) + } + + return nil +} + +// desiredServiceMonitor returns the desired ServiceMonitor as an unstructured object. +// It creates one endpoint per zone container, scraping HTTPS directly from each ExternalDNS container. +func desiredServiceMonitor(namespace string, externalDNS *operatorv1beta1.ExternalDNS) *unstructured.Unstructured { + smName := controller.ExternalDNSServiceMonitorName(externalDNS) + serviceName := controller.ExternalDNSMetricsServiceName(externalDNS) + serverName := fmt.Sprintf("%s.%s.svc", serviceName, namespace) + + numZones := numMetricsPorts(externalDNS) + endpoints := make([]interface{}, numZones) + for i := 0; i < numZones; i++ { + endpoints[i] = map[string]interface{}{ + "interval": "30s", + "path": "/metrics", + "port": metricsPortNameForSeq(i), + "scheme": "https", + "bearerTokenFile": "/var/run/secrets/kubernetes.io/serviceaccount/token", + "tlsConfig": map[string]interface{}{ + "caFile": "/etc/prometheus/configmaps/serving-certs-ca-bundle/service-ca.crt", + "serverName": serverName, + }, + } + } + + sm := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "monitoring.coreos.com/v1", + "kind": "ServiceMonitor", + "metadata": map[string]interface{}{ + "name": smName, + "namespace": namespace, + "labels": map[string]interface{}{ + appNameLabel: controller.ExternalDNSBaseName, + appInstanceLabel: externalDNS.Name, + }, + }, + "spec": map[string]interface{}{ + "endpoints": endpoints, + "namespaceSelector": map[string]interface{}{ + "matchNames": []interface{}{ + namespace, + }, + }, + "selector": map[string]interface{}{ + "matchLabels": map[string]interface{}{ + appNameLabel: controller.ExternalDNSBaseName, + appInstanceLabel: externalDNS.Name, + }, + }, + }, + }, + } + sm.SetGroupVersionKind(serviceMonitorGVK) + return sm +} + +// serviceMonitorChanged returns true if the fields we manage have drifted +// between the current and desired ServiceMonitor objects. +func serviceMonitorChanged(current, desired *unstructured.Unstructured) bool { + desiredLabels := desired.GetLabels() + currentLabels := current.GetLabels() + for k, v := range desiredLabels { + if currentLabels[k] != v { + return true + } + } + for _, field := range []string{"endpoints", "selector", "namespaceSelector"} { + currentVal, _, _ := unstructured.NestedFieldNoCopy(current.Object, "spec", field) + desiredVal, _, _ := unstructured.NestedFieldNoCopy(desired.Object, "spec", field) + if !reflect.DeepEqual(currentVal, desiredVal) { + return true + } + } + return false +} diff --git a/pkg/operator/controller/externaldns/servicemonitor_test.go b/pkg/operator/controller/externaldns/servicemonitor_test.go new file mode 100644 index 000000000..6eb934fd3 --- /dev/null +++ b/pkg/operator/controller/externaldns/servicemonitor_test.go @@ -0,0 +1,206 @@ +/* +Copyright 2026. + +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 externaldnscontroller + +import ( + "fmt" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + operatorv1beta1 "github.com/openshift/external-dns-operator/api/v1beta1" + controller "github.com/openshift/external-dns-operator/pkg/operator/controller" + "github.com/openshift/external-dns-operator/pkg/operator/controller/utils/test" +) + +func TestDesiredServiceMonitor(t *testing.T) { + testCases := []struct { + name string + externalDNS *operatorv1beta1.ExternalDNS + namespace string + expectedEndpoints int + }{ + { + name: "single zone AWS", + externalDNS: testAWSExternalDNS(operatorv1beta1.SourceTypeService), + namespace: test.OperandNamespace, + expectedEndpoints: 1, + }, + { + name: "multiple zones AWS", + externalDNS: testAWSExternalDNSZones([]string{test.PublicZone, test.PrivateZone}, operatorv1beta1.SourceTypeService), + namespace: test.OperandNamespace, + expectedEndpoints: 2, + }, + { + name: "Azure no zones creates 2 endpoints", + externalDNS: testAzureExternalDNSNoZones(operatorv1beta1.SourceTypeService), + namespace: test.OperandNamespace, + expectedEndpoints: 2, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sm := desiredServiceMonitor(tc.namespace, tc.externalDNS) + + // Verify GVK. + if sm.GetKind() != "ServiceMonitor" { + t.Errorf("expected kind ServiceMonitor, got %s", sm.GetKind()) + } + if sm.GetAPIVersion() != "monitoring.coreos.com/v1" { + t.Errorf("expected apiVersion monitoring.coreos.com/v1, got %s", sm.GetAPIVersion()) + } + + // Verify name and namespace. + expectedName := controller.ExternalDNSServiceMonitorName(tc.externalDNS) + if sm.GetName() != expectedName { + t.Errorf("expected name %q, got %q", expectedName, sm.GetName()) + } + if sm.GetNamespace() != tc.namespace { + t.Errorf("expected namespace %q, got %q", tc.namespace, sm.GetNamespace()) + } + + // Verify endpoints count. + endpoints, found, err := unstructured.NestedSlice(sm.Object, "spec", "endpoints") + if err != nil || !found { + t.Fatalf("failed to get endpoints from service monitor: found=%v, err=%v", found, err) + } + if len(endpoints) != tc.expectedEndpoints { + t.Errorf("expected %d endpoints, got %d", tc.expectedEndpoints, len(endpoints)) + } + + // Verify each endpoint has correct fields. + serviceName := controller.ExternalDNSMetricsServiceName(tc.externalDNS) + expectedServerName := fmt.Sprintf("%s.%s.svc", serviceName, tc.namespace) + for i, ep := range endpoints { + epMap, ok := ep.(map[string]interface{}) + if !ok { + t.Fatalf("endpoint %d is not a map", i) + } + if epMap["scheme"] != "https" { + t.Errorf("endpoint %d: expected scheme https, got %v", i, epMap["scheme"]) + } + if epMap["path"] != "/metrics" { + t.Errorf("endpoint %d: expected path /metrics, got %v", i, epMap["path"]) + } + expectedPort := metricsPortNameForSeq(i) + if epMap["port"] != expectedPort { + t.Errorf("endpoint %d: expected port %q, got %v", i, expectedPort, epMap["port"]) + } + tlsConfig, ok := epMap["tlsConfig"].(map[string]interface{}) + if !ok { + t.Fatalf("endpoint %d: tlsConfig missing or wrong type", i) + } + if tlsConfig["serverName"] != expectedServerName { + t.Errorf("endpoint %d: expected serverName %q, got %v", i, expectedServerName, tlsConfig["serverName"]) + } + } + + // Verify selector labels. + matchLabels, found, err := unstructured.NestedStringMap(sm.Object, "spec", "selector", "matchLabels") + if err != nil || !found { + t.Fatalf("failed to get selector matchLabels: found=%v, err=%v", found, err) + } + if matchLabels[appNameLabel] != controller.ExternalDNSBaseName { + t.Errorf("expected selector label %s=%s, got %s", appNameLabel, controller.ExternalDNSBaseName, matchLabels[appNameLabel]) + } + if matchLabels[appInstanceLabel] != tc.externalDNS.Name { + t.Errorf("expected selector label %s=%s, got %s", appInstanceLabel, tc.externalDNS.Name, matchLabels[appInstanceLabel]) + } + }) + } +} + +func TestServiceMonitorChanged(t *testing.T) { + extDNS := &operatorv1beta1.ExternalDNS{ + ObjectMeta: metav1.ObjectMeta{Name: test.Name}, + Spec: operatorv1beta1.ExternalDNSSpec{ + Provider: operatorv1beta1.ExternalDNSProvider{Type: operatorv1beta1.ProviderTypeAWS}, + Zones: []string{test.PublicZone}, + }, + } + base := desiredServiceMonitor(test.OperandNamespace, extDNS) + + testCases := []struct { + name string + mutate func(*unstructured.Unstructured) + changed bool + }{ + { + name: "no change", + mutate: func(sm *unstructured.Unstructured) {}, + changed: false, + }, + { + name: "label changed", + mutate: func(sm *unstructured.Unstructured) { + labels := sm.GetLabels() + labels[appInstanceLabel] = "different" + sm.SetLabels(labels) + }, + changed: true, + }, + { + name: "extra defaulted field in spec does not trigger change", + mutate: func(sm *unstructured.Unstructured) { + // Simulate API server adding a defaulted field we don't manage. + _ = unstructured.SetNestedField(sm.Object, "None", "spec", "targetLabels") + }, + changed: false, + }, + { + name: "endpoints changed", + mutate: func(sm *unstructured.Unstructured) { + endpoints, _, _ := unstructured.NestedSlice(sm.Object, "spec", "endpoints") + if len(endpoints) > 0 { + ep := endpoints[0].(map[string]interface{}) + ep["interval"] = "60s" + _ = unstructured.SetNestedSlice(sm.Object, endpoints, "spec", "endpoints") + } + }, + changed: true, + }, + { + name: "selector changed", + mutate: func(sm *unstructured.Unstructured) { + _ = unstructured.SetNestedField(sm.Object, "different", "spec", "selector", "matchLabels", appInstanceLabel) + }, + changed: true, + }, + { + name: "namespaceSelector changed", + mutate: func(sm *unstructured.Unstructured) { + _ = unstructured.SetNestedStringSlice(sm.Object, []string{"other-ns"}, "spec", "namespaceSelector", "matchNames") + }, + changed: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + current := base.DeepCopy() + tc.mutate(current) + + got := serviceMonitorChanged(current, desiredServiceMonitor(test.OperandNamespace, extDNS)) + if got != tc.changed { + t.Errorf("expected changed=%v, got %v", tc.changed, got) + } + }) + } +} diff --git a/pkg/operator/controller/names.go b/pkg/operator/controller/names.go index 9239dc418..cdd12b53b 100644 --- a/pkg/operator/controller/names.go +++ b/pkg/operator/controller/names.go @@ -62,6 +62,21 @@ func ExternalDNSGlobalResourceName() string { return ExternalDNSBaseName } +// ExternalDNSMetricsServiceName returns the name for the metrics service of the given ExternalDNS instance. +func ExternalDNSMetricsServiceName(externalDNS *operatorv1beta1.ExternalDNS) string { + return truncatedName(ExternalDNSBaseName, externalDNS.Name, "-metrics") +} + +// ExternalDNSServiceMonitorName returns the name for the service monitor of the given ExternalDNS instance. +func ExternalDNSServiceMonitorName(externalDNS *operatorv1beta1.ExternalDNS) string { + return truncatedName(ExternalDNSBaseName, externalDNS.Name, "-metrics-monitor") +} + +// ExternalDNSMetricsSecretName returns the name for the metrics serving cert secret of the given ExternalDNS instance. +func ExternalDNSMetricsSecretName(externalDNS *operatorv1beta1.ExternalDNS) string { + return truncatedName(ExternalDNSBaseName, externalDNS.Name, "-metrics-cert") +} + // ExternalDNSContainerName returns the container name unique for the given DNS zone. func ExternalDNSContainerName(zone string) string { return ExternalDNSBaseName + "-" + hashString(zone) @@ -115,6 +130,21 @@ func ExternalDNSCredentialsSecretNameFromProvider(externalDNS *operatorv1beta1.E return "" } +const maxDNSLabelLength = 63 + +func truncatedName(base, name, suffix string) string { + result := base + "-" + name + suffix + if len(result) <= maxDNSLabelLength { + return result + } + h := hashString(name) + available := maxDNSLabelLength - len(base) - len(suffix) - len(h) - 2 + if available < 0 { + available = 0 + } + return base + "-" + name[:available] + "-" + h + suffix +} + func hashString(str string) string { hasher := getHasher() hasher.Write([]byte(str)) diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go index 7a810e334..e9b04259d 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -51,8 +51,10 @@ type Operator struct { // +kubebuilder:rbac:groups=config.openshift.io,resources=infrastructures,verbs=get;list;watch // local role // +kubebuilder:rbac:groups="",namespace=external-dns-operator,resources=secrets;serviceaccounts;configmaps,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups="",namespace=external-dns-operator,resources=services,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups="apps",namespace=external-dns-operator,resources=deployments,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups="",namespace=external-dns-operator,resources=pods,verbs=get;list;watch +// +kubebuilder:rbac:groups=monitoring.coreos.com,namespace=external-dns-operator,resources=servicemonitors,verbs=get;list;watch;create;update;patch;delete // New creates a new operator from cliCfg and opCfg. func New(cliCfg *rest.Config, opCfg *operatorconfig.Config) (*Operator, error) {