From 0e7f95a3f65a10cf487e2054d7629c51c4f569fe Mon Sep 17 00:00:00 2001 From: Martin Schuppert Date: Tue, 5 May 2026 17:32:01 +0200 Subject: [PATCH] [dnsmasq] Default local=/.svc/ to resolve queries locally Set local=/.svc/ by default in DNSMasq webhook so that queries for the openstack.svc domain are resolved from hosts files rather than forwarded to upstream DNS servers. This prevents delays and SERVFAIL errors when upstream DNS is slow at resolving AAAA records for openstack.svc. The namespace is passed dynamically through the Default() chain so the domain is correct for any deployment namespace. Users can override the default by specifying their own local option. Existing CRs get the default on upgrade via controller-triggered webhook (EnsureWebhookTrigger pattern from lib-common). Closes: OSPRH-29710 Co-Authored-By: Claude Opus 4.6 Signed-off-by: Martin Schuppert --- apis/go.mod | 5 +- apis/go.sum | 13 ++-- apis/network/v1beta1/dnsmasq_webhook.go | 37 ++++++++--- go.mod | 8 +-- go.sum | 16 ++--- .../controller/network/dnsmasq_controller.go | 26 ++++++++ test/functional/dnsmasq_controller_test.go | 61 +++++++++++++++++++ 7 files changed, 137 insertions(+), 29 deletions(-) diff --git a/apis/go.mod b/apis/go.mod index dcfb67c4..9ad32304 100644 --- a/apis/go.mod +++ b/apis/go.mod @@ -4,8 +4,8 @@ go 1.24.4 require ( github.com/go-logr/logr v1.4.3 - github.com/onsi/gomega v1.39.1 - github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981 + github.com/onsi/gomega v1.40.0 + github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260506154724-30a976ba8ef0 k8s.io/api v0.31.14 k8s.io/apiextensions-apiserver v0.33.2 k8s.io/apimachinery v0.31.14 @@ -40,7 +40,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/onsi/ginkgo/v2 v2.28.2 // indirect github.com/openshift/api v3.9.0+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.22.0 // indirect diff --git a/apis/go.sum b/apis/go.sum index 0ebeeee0..5bed0a7a 100644 --- a/apis/go.sum +++ b/apis/go.sum @@ -76,14 +76,15 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo/v2 v2.28.2 h1:DTrMfpqxiNUyQ3Y0zhn1n3cOO2euFgQPYIpkWwxVFps= github.com/onsi/ginkgo/v2 v2.28.2/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= -github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= -github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= +github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc= +github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e h1:E1OdwSpqWuDPCedyUt0GEdoAE+r5TXy7YS21yNEo+2U= github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e/go.mod h1:Shkl4HanLwDiiBzakv+con/aMGnVE2MAGvoKp5oyYUo= -github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981 h1:v1viH0gmNb+AXMg/0GxDcj8VUTdjVLotfOIGrNyMxHk= -github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981/go.mod h1:I/VBXZLdjk8DUGsEbB+Ha72JBFYYntP7Pm2FpEto9K8= +github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260506154724-30a976ba8ef0 h1:vkFvn06Ns9qW4AbzFjFDu8ioosRmhkEZiDrO3DOQhLg= +github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260506154724-30a976ba8ef0/go.mod h1:aIuG6lx3aS0vnXweRNdR/Q0SlfOsLIo0OzrqKK7C6xs= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -113,8 +114,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= -go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= diff --git a/apis/network/v1beta1/dnsmasq_webhook.go b/apis/network/v1beta1/dnsmasq_webhook.go index b6899d8e..47ade3d5 100644 --- a/apis/network/v1beta1/dnsmasq_webhook.go +++ b/apis/network/v1beta1/dnsmasq_webhook.go @@ -17,13 +17,16 @@ limitations under the License. package v1beta1 import ( + "fmt" + + annotations "github.com/openstack-k8s-operators/lib-common/modules/common/annotations" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/validation/field" ) // DNSMasqDefaults - @@ -48,20 +51,38 @@ var _ webhook.Defaulter = &DNSMasq{} func (r *DNSMasq) Default() { dnsmasqlog.Info("default", "name", r.Name) - r.Spec.Default() + r.Spec.Default(r.Namespace) + + ann := r.GetAnnotations() + if _, exists := ann[annotations.ReconcileTriggerAnnotation]; exists { + delete(ann, annotations.ReconcileTriggerAnnotation) + r.SetAnnotations(ann) + } } // Default - set defaults for this DNSMasq spec -func (spec *DNSMasqSpec) Default() { +func (spec *DNSMasqSpec) Default(namespace string) { if spec.ContainerImage == "" { spec.ContainerImage = dnsMasqDefaults.ContainerImageURL } - spec.DNSMasqSpecCore.Default() + spec.DNSMasqSpecCore.Default(namespace) } // Default - common defaults go here (for the OpenStackControlplane which uses this one) -func (spec *DNSMasqSpecCore) Default() { - // nothing here +func (spec *DNSMasqSpecCore) Default(namespace string) { + hasLocal := false + for _, opt := range spec.Options { + if opt.Key == "local" { + hasLocal = true + break + } + } + if !hasLocal { + spec.Options = append(spec.Options, DNSMasqOption{ + Key: "local", + Values: []string{fmt.Sprintf("/%s.svc/", namespace)}, + }) + } } var _ webhook.Validator = &DNSMasq{} diff --git a/go.mod b/go.mod index e9d8af79..640ee3f6 100644 --- a/go.mod +++ b/go.mod @@ -8,12 +8,12 @@ require ( github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.7.7 github.com/metallb/frr-k8s v0.0.15 github.com/onsi/ginkgo/v2 v2.28.2 - github.com/onsi/gomega v1.39.1 + github.com/onsi/gomega v1.40.0 github.com/openshift/api v3.9.0+incompatible github.com/openstack-k8s-operators/infra-operator/apis v0.0.0-00010101000000-000000000000 - github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981 - github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20260417092244-81c71b39e981 - go.uber.org/zap v1.27.1 + github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260506154724-30a976ba8ef0 + github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20260506154724-30a976ba8ef0 + go.uber.org/zap v1.28.0 golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 k8s.io/api v0.31.14 k8s.io/apiextensions-apiserver v0.33.2 diff --git a/go.sum b/go.sum index 6fc1b14f..60625b48 100644 --- a/go.sum +++ b/go.sum @@ -114,14 +114,14 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.28.2 h1:DTrMfpqxiNUyQ3Y0zhn1n3cOO2euFgQPYIpkWwxVFps= github.com/onsi/ginkgo/v2 v2.28.2/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= -github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= -github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= +github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc= +github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e h1:E1OdwSpqWuDPCedyUt0GEdoAE+r5TXy7YS21yNEo+2U= github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e/go.mod h1:Shkl4HanLwDiiBzakv+con/aMGnVE2MAGvoKp5oyYUo= -github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981 h1:v1viH0gmNb+AXMg/0GxDcj8VUTdjVLotfOIGrNyMxHk= -github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981/go.mod h1:I/VBXZLdjk8DUGsEbB+Ha72JBFYYntP7Pm2FpEto9K8= -github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20260417092244-81c71b39e981 h1:KAQ8T+Ri3JWgsyK1D6QybScMh6fpkYUUA+0ntnOiAl4= -github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20260417092244-81c71b39e981/go.mod h1:dEjz8zHRIlP3vnMmWdHytlLeSZ6BHcIiSTPM7xTQxFg= +github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260506154724-30a976ba8ef0 h1:vkFvn06Ns9qW4AbzFjFDu8ioosRmhkEZiDrO3DOQhLg= +github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260506154724-30a976ba8ef0/go.mod h1:aIuG6lx3aS0vnXweRNdR/Q0SlfOsLIo0OzrqKK7C6xs= +github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20260506154724-30a976ba8ef0 h1:mG3QhS/QWv9Y/AkZZ5OzO6hu6+l5oDXnI/Q5ZUbj6Zs= +github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20260506154724-30a976ba8ef0/go.mod h1:ZYG9CQe7cOePOKQbenEZFA28kPdkUOe9QKbDRwGhEV0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -186,8 +186,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= -go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= diff --git a/internal/controller/network/dnsmasq_controller.go b/internal/controller/network/dnsmasq_controller.go index f4c2adfa..9630e752 100644 --- a/internal/controller/network/dnsmasq_controller.go +++ b/internal/controller/network/dnsmasq_controller.go @@ -49,6 +49,7 @@ import ( topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" dnsmasq "github.com/openstack-k8s-operators/infra-operator/internal/dnsmasq" common "github.com/openstack-k8s-operators/lib-common/modules/common" + annotations "github.com/openstack-k8s-operators/lib-common/modules/common/annotations" condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" configmap "github.com/openstack-k8s-operators/lib-common/modules/common/configmap" deployment "github.com/openstack-k8s-operators/lib-common/modules/common/deployment" @@ -58,6 +59,7 @@ import ( common_rbac "github.com/openstack-k8s-operators/lib-common/modules/common/rbac" service "github.com/openstack-k8s-operators/lib-common/modules/common/service" util "github.com/openstack-k8s-operators/lib-common/modules/common/util" + "github.com/openstack-k8s-operators/lib-common/modules/common/webhook" ) // DNSMasqReconciler reconciles a DNSMasq object @@ -375,6 +377,30 @@ func (r *DNSMasqReconciler) reconcileNormal(ctx context.Context, instance *netwo Log := r.GetLogger(ctx) Log.Info("Reconciling Service") + // Ensure existing CRs get the default local=/.svc/ option on + // upgrade by triggering the webhook via a reconcile-trigger annotation. + hasLocal := false + for _, opt := range instance.Spec.Options { + if opt.Key == "local" { + hasLocal = true + break + } + } + if !hasLocal { + result, err := webhook.EnsureWebhookTrigger( + ctx, + instance, + annotations.ReconcileTriggerAnnotation, + "DNSMasq local option defaulting", + Log, + 0, // use default 5 minute timeout + ) + if err != nil { + return ctrl.Result{}, err + } + return result, nil + } + serviceLabels := map[string]string{ common.AppSelector: dnsmasq.ServiceName, } diff --git a/test/functional/dnsmasq_controller_test.go b/test/functional/dnsmasq_controller_test.go index 2ca851b8..5d9f88af 100644 --- a/test/functional/dnsmasq_controller_test.go +++ b/test/functional/dnsmasq_controller_test.go @@ -25,6 +25,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" + networkv1 "github.com/openstack-k8s-operators/infra-operator/apis/network/v1beta1" topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" "github.com/openstack-k8s-operators/lib-common/modules/common/util" @@ -141,6 +142,8 @@ var _ = Describe("DNSMasq controller", func() { ContainSubstring("server=1.1.1.1")) Expect(configData.Data[dnsMasqName.Name]).Should( ContainSubstring("no-negcache\n")) + Expect(configData.Data[dnsMasqName.Name]).Should( + ContainSubstring(fmt.Sprintf("local=/%s.svc/", namespace))) Expect(configData.Labels["dnsmasq.openstack.org/name"]).To(Equal(dnsMasqName.Name)) }) @@ -273,6 +276,64 @@ var _ = Describe("DNSMasq controller", func() { }) }) + When("A DNSMasq is created with a custom local option", func() { + BeforeEach(func() { + spec := GetDefaultDNSMasqSpec() + spec["options"] = any([]networkv1.DNSMasqOption{ + { + Key: "server", + Values: []string{"1.1.1.1"}, + }, + { + Key: "no-negcache", + Values: []string{}, + }, + { + Key: "local", + Values: []string{"/custom.svc/"}, + }, + }) + instance := CreateDNSMasq(namespace, spec) + dnsMasqName = types.NamespacedName{ + Name: instance.GetName(), + Namespace: namespace, + } + + dnsDataCM = types.NamespacedName{ + Namespace: namespace, + Name: "some-dnsdata", + } + + th.CreateConfigMap(dnsDataCM, map[string]any{ + dnsDataCM.Name: "172.20.0.80 keystone-internal.openstack.svc", + }) + cm := th.GetConfigMap(dnsDataCM) + cm.Labels = util.MergeStringMaps(cm.Labels, map[string]string{ + "dnsmasqhosts": "dnsdata", + }) + Expect(th.K8sClient.Update(ctx, cm)).Should(Succeed()) + + DeferCleanup(th.DeleteConfigMap, dnsDataCM) + DeferCleanup(th.DeleteInstance, instance) + }) + + It("uses the custom local value and does not add the default", func() { + th.ExpectCondition( + dnsMasqName, + ConditionGetterFunc(DNSMasqConditionGetter), + condition.ServiceConfigReadyCondition, + corev1.ConditionTrue, + ) + + configData := th.GetConfigMap(dnsMasqName) + Expect(configData).ShouldNot(BeNil()) + Expect(configData.Data[dnsMasqName.Name]).Should( + ContainSubstring("local=/custom.svc/")) + Expect(configData.Data[dnsMasqName.Name]).ShouldNot( + ContainSubstring(fmt.Sprintf("local=/%s.svc/", namespace))) + }) + }) + When("Deployment rollout is progressing", func() { BeforeEach(func() { instance := CreateDNSMasq(namespace, GetDefaultDNSMasqSpec())