From 727d3965dc74a458e8e6ca3cf9742302c9d00b57 Mon Sep 17 00:00:00 2001 From: Luca Miccini Date: Tue, 12 May 2026 09:15:59 +0200 Subject: [PATCH 1/3] Add edpm module with unstructured NodeSet secret hash check Add a new modules/edpm package that checks whether OpenStackDataPlaneNodeSet deployed secret hashes are in sync with current cluster secrets. Uses an unstructured client to avoid importing dataplane API types, allowing consumers like infra-operator to drop the heavyweight openstack-operator/api dependency. Co-Authored-By: Claude Opus 4.6 --- modules/edpm/go.mod | 74 +++++++++++++ modules/edpm/go.sum | 195 ++++++++++++++++++++++++++++++++++ modules/edpm/nodeset.go | 135 ++++++++++++++++++++++++ modules/edpm/nodeset_test.go | 198 +++++++++++++++++++++++++++++++++++ 4 files changed, 602 insertions(+) create mode 100644 modules/edpm/go.mod create mode 100644 modules/edpm/go.sum create mode 100644 modules/edpm/nodeset.go create mode 100644 modules/edpm/nodeset_test.go diff --git a/modules/edpm/go.mod b/modules/edpm/go.mod new file mode 100644 index 00000000..2483a78e --- /dev/null +++ b/modules/edpm/go.mod @@ -0,0 +1,74 @@ +module github.com/openstack-k8s-operators/lib-common/modules/edpm + +go 1.24.4 + +require ( + github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260417092244-81c71b39e981 + k8s.io/api v0.31.14 + k8s.io/apimachinery v0.31.14 + sigs.k8s.io/controller-runtime v0.19.7 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.1 // indirect + github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.20.4 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/time v0.6.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/protobuf v1.36.7 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.31.14 // indirect + k8s.io/client-go v0.31.14 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 // indirect + k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) + +replace github.com/openstack-k8s-operators/lib-common/modules/common => ../common + +// mschuppert: map to latest commit from release-4.18 tag +// must consistent within modules and service operators +replace github.com/openshift/api => github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e diff --git a/modules/edpm/go.sum b/modules/edpm/go.sum new file mode 100644 index 00000000..11f6b439 --- /dev/null +++ b/modules/edpm/go.sum @@ -0,0 +1,195 @@ +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= +github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= +github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= +github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/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/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.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc= +github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= +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= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.31.14 h1:xYn/S/WFJsksI7dk/5uBRd3Umm/D8W5g7sRnd4csotA= +k8s.io/api v0.31.14/go.mod h1:K8fvRey4z73RAuxBZCma7WtY8WFvkViYhfFLCMT4xgA= +k8s.io/apiextensions-apiserver v0.31.14 h1:1KupD0PyU7CgiT/PiZPSgZhTCL2KGwvXd1ejGcxjEfg= +k8s.io/apiextensions-apiserver v0.31.14/go.mod h1:Odk14fSl/zaciI8DRUSPMSH74UXtz4gfinw7zY7YHvE= +k8s.io/apimachinery v0.31.14 h1:/eMIwjv+GFm6A/sSGlB1NupBU6wTDPhEWsju0Fj69kY= +k8s.io/apimachinery v0.31.14/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.14 h1:d4/G0xfksNIbMWH7ghjzOwC5bTAwQ20gABTjZw7fLlQ= +k8s.io/client-go v0.31.14/go.mod h1:0uRpRB7r5QwtsbxEngZPkbcIVoNdAQAPIcopgiXjhQc= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 h1:1dWzkmJrrprYvjGwh9kEUxmcUV/CtNU8QM7h1FLWQOo= +k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38/go.mod h1:coRQXBK9NxO98XUv3ZD6AK3xzHCxV6+b7lrquKwaKzA= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.19.7 h1:DLABZfMr20A+AwCZOHhcbcu+TqBXnJZaVBri9K3EO48= +sigs.k8s.io/controller-runtime v0.19.7/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/modules/edpm/nodeset.go b/modules/edpm/nodeset.go new file mode 100644 index 00000000..92c79f93 --- /dev/null +++ b/modules/edpm/nodeset.go @@ -0,0 +1,135 @@ +/* +Copyright 2026 Red Hat + +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 edpm + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + k8s_errors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "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/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + oko_secret "github.com/openstack-k8s-operators/lib-common/modules/common/secret" +) + +// NodeSetGVK is the GroupVersionKind for OpenStackDataPlaneNodeSet. +// Exported so callers can use it for controller watches on unstructured objects. +var NodeSetGVK = schema.GroupVersionKind{ + Group: "dataplane.openstack.org", + Version: "v1beta1", + Kind: "OpenStackDataPlaneNodeSet", +} + +// NewNodeSetObject returns an unstructured OpenStackDataPlaneNodeSet object, +// suitable for use with controller-runtime Watches. +func NewNodeSetObject() *unstructured.Unstructured { + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(NodeSetGVK) + return obj +} + +// AreSecretHashesInSync checks whether the deployed secret hashes in all +// OpenStackDataPlaneNodeSets in the given namespace match the current cluster +// secrets. It uses an unstructured client to avoid importing dataplane API types. +// +// Returns: +// - inSync=true when all hashes match, no nodesets exist, or the +// OpenStackDataPlaneNodeSet CRD is not installed on the cluster. +// - inSync=false with info describing the first mismatch when a secret +// has changed since the last full deployment or has been deleted. +func AreSecretHashesInSync( + ctx context.Context, + c client.Client, + namespace string, +) (inSync bool, info string, err error) { + Log := log.FromContext(ctx) + + nodesetList := &unstructured.UnstructuredList{} + nodesetList.SetGroupVersionKind(schema.GroupVersionKind{ + Group: NodeSetGVK.Group, + Version: NodeSetGVK.Version, + Kind: NodeSetGVK.Kind + "List", + }) + + if err := c.List(ctx, nodesetList, client.InNamespace(namespace)); err != nil { + if meta.IsNoMatchError(err) { + Log.Info("OpenStackDataPlaneNodeSet CRD not installed, skipping hash check") + return true, "", nil + } + return false, "", fmt.Errorf("failed to list OpenStackDataPlaneNodeSets: %w", err) + } + + if len(nodesetList.Items) == 0 { + Log.Info("No nodesets found in namespace - secrets in sync", + "namespace", namespace) + return true, "", nil + } + + for i := range nodesetList.Items { + item := &nodesetList.Items[i] + + if err := ctx.Err(); err != nil { + return false, "", fmt.Errorf("context cancelled during nodeset check: %w", err) + } + + secretHashes, found, err := unstructured.NestedStringMap(item.Object, "status", "secretHashes") + if err != nil { + return false, "", fmt.Errorf("failed to read secretHashes from nodeset %s/%s: %w", + item.GetNamespace(), item.GetName(), err) + } + if !found || len(secretHashes) == 0 { + continue + } + + for secretName, deployedHash := range secretHashes { + currentSecret := &corev1.Secret{} + err := c.Get(ctx, types.NamespacedName{ + Name: secretName, + Namespace: namespace, + }, currentSecret) + if err != nil { + if k8s_errors.IsNotFound(err) { + info := fmt.Sprintf("nodeset %s/%s: deployed secret %s no longer exists", + item.GetNamespace(), item.GetName(), secretName) + return false, info, nil + } + return false, "", fmt.Errorf("failed to get secret %s: %w", secretName, err) + } + + currentHash, hashErr := oko_secret.Hash(currentSecret) + if hashErr != nil { + return false, "", fmt.Errorf("failed to hash secret %s: %w", secretName, hashErr) + } + + if currentHash != deployedHash { + info := fmt.Sprintf("nodeset %s/%s: secret %s has changed since last full deployment", + item.GetNamespace(), item.GetName(), secretName) + return false, info, nil + } + } + } + + Log.Info("All nodeset secret hashes match - secrets in sync", + "namespace", namespace, "nodesetsChecked", len(nodesetList.Items)) + return true, "", nil +} diff --git a/modules/edpm/nodeset_test.go b/modules/edpm/nodeset_test.go new file mode 100644 index 00000000..4b085fe6 --- /dev/null +++ b/modules/edpm/nodeset_test.go @@ -0,0 +1,198 @@ +/* +Copyright 2025 Red Hat + +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 edpm + +import ( + "context" + "strings" + "testing" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + oko_secret "github.com/openstack-k8s-operators/lib-common/modules/common/secret" +) + +func makeNodeSet(name, namespace string, secretHashes map[string]string) *unstructured.Unstructured { + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(NodeSetGVK) + obj.SetName(name) + obj.SetNamespace(namespace) + if len(secretHashes) > 0 { + hashes := map[string]interface{}{} + for k, v := range secretHashes { + hashes[k] = v + } + obj.Object["status"] = map[string]interface{}{ + "secretHashes": hashes, + } + } + return obj +} + +func newTestSchemeAndMapper() (*runtime.Scheme, meta.RESTMapper) { + s := runtime.NewScheme() + _ = corev1.AddToScheme(s) + + mapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{ + {Group: "dataplane.openstack.org", Version: "v1beta1"}, + }) + mapper.Add(NodeSetGVK, meta.RESTScopeNamespace) + + return s, mapper +} + +func TestAreSecretHashesInSync(t *testing.T) { + currentSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nova-cell1-compute-config", + Namespace: "test", + }, + Data: map[string][]byte{"transport_url": []byte("rabbit://nova:current-password@rabbitmq:5672/")}, + } + currentHash, _ := oko_secret.Hash(currentSecret) + + tests := []struct { + name string + nodesets []*unstructured.Unstructured + secrets []*corev1.Secret + wantInSync bool + wantInfoSubstr string + wantErr bool + }{ + { + name: "no nodesets exist", + wantInSync: true, + }, + { + name: "nodeset with stale secrets is out of sync", + nodesets: []*unstructured.Unstructured{ + makeNodeSet("test-nodeset", "test", map[string]string{ + "nova-cell1-compute-config": "old-stale-hash", + }), + }, + secrets: []*corev1.Secret{currentSecret}, + wantInSync: false, + wantInfoSubstr: "has changed since last full deployment", + }, + { + name: "nodeset with current secrets is in sync", + nodesets: []*unstructured.Unstructured{ + makeNodeSet("test-nodeset", "test", map[string]string{ + "nova-cell1-compute-config": currentHash, + }), + }, + secrets: []*corev1.Secret{currentSecret}, + wantInSync: true, + }, + { + name: "nodeset with empty SecretHashes is in sync", + nodesets: []*unstructured.Unstructured{ + makeNodeSet("never-deployed-nodeset", "test", map[string]string{}), + }, + wantInSync: true, + }, + { + name: "deployed secret deleted is out of sync", + nodesets: []*unstructured.Unstructured{ + makeNodeSet("test-nodeset", "test", map[string]string{ + "deleted-secret": "some-hash", + }), + }, + secrets: []*corev1.Secret{}, + wantInSync: false, + wantInfoSubstr: "no longer exists", + }, + { + name: "transport URL secret updated after credential change is out of sync", + nodesets: []*unstructured.Unstructured{ + makeNodeSet("edpm-compute", "test", map[string]string{ + "nova-cell1-transport": "hash-with-old-credentials", + }), + }, + secrets: []*corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{Name: "nova-cell1-transport", Namespace: "test"}, + Data: map[string][]byte{"transport_url": []byte("rabbit://novacell2:newpass@rabbitmq:5672/")}, + }, + }, + wantInSync: false, + wantInfoSubstr: "has changed since last full deployment", + }, + { + name: "multiple nodesets - one stale blocks sync", + nodesets: []*unstructured.Unstructured{ + makeNodeSet("up-to-date-nodeset", "test", map[string]string{ + "nova-cell1-compute-config": currentHash, + }), + makeNodeSet("stale-nodeset", "test", map[string]string{ + "nova-cell1-compute-config": "old-hash-from-previous-deployment", + }), + }, + secrets: []*corev1.Secret{currentSecret}, + wantInSync: false, + wantInfoSubstr: "has changed since last full deployment", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, mapper := newTestSchemeAndMapper() + + builder := fake.NewClientBuilder(). + WithScheme(s). + WithRESTMapper(mapper) + + for _, ns := range tt.nodesets { + builder = builder.WithObjects(ns) + } + for _, sec := range tt.secrets { + builder = builder.WithObjects(sec) + } + + c := builder.Build() + + inSync, info, err := AreSecretHashesInSync( + context.Background(), + c, + "test", + ) + + if (err != nil) != tt.wantErr { + t.Errorf("AreSecretHashesInSync() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if inSync != tt.wantInSync { + t.Errorf("AreSecretHashesInSync() inSync = %v, want %v (info: %s)", inSync, tt.wantInSync, info) + } + + if tt.wantInfoSubstr != "" { + if info == "" { + t.Errorf("AreSecretHashesInSync() info is empty, want substring %q", tt.wantInfoSubstr) + } else if !strings.Contains(info, tt.wantInfoSubstr) { + t.Errorf("AreSecretHashesInSync() info = %q, want substring %q", info, tt.wantInfoSubstr) + } + } + }) + } +} From 42bc2fd66eef64d8ba2b2fd6fb77ef3654155d72 Mon Sep 17 00:00:00 2001 From: Luca Miccini Date: Tue, 12 May 2026 11:12:06 +0200 Subject: [PATCH 2/3] Add dataplane NodeSet CRD and GetDataPlaneCRDDir helper Ship a minimal OpenStackDataPlaneNodeSet CRD in the test module so operators using the edpm package can load it in envtest without importing openstack-operator/api. Co-Authored-By: Claude Opus 4.6 --- modules/test/crd.go | 22 ++++++++++ ...nstack.org_openstackdataplanenodesets.yaml | 41 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 modules/test/dataplane_crds/dataplane.openstack.org_openstackdataplanenodesets.yaml diff --git a/modules/test/crd.go b/modules/test/crd.go index a7ae281d..dcd5c1ec 100644 --- a/modules/test/crd.go +++ b/modules/test/crd.go @@ -133,6 +133,28 @@ func GetCRDDirFromModule(moduleName string, goModPath string, relativeCRDPath st return path, nil } +// GetDataPlaneCRDDir returns the absolute path of the directory holding the +// dataplane custom resource definitions. It will look the CRD path up from +// the lib-common test module, similar to GetOpenShiftCRDDir. +func GetDataPlaneCRDDir(goModPath string) (string, error) { + libCommon := "github.com/openstack-k8s-operators/lib-common/modules/test" + libCommon, version, err := getDependencyVersion(libCommon, goModPath) + if err != nil { + return "", err + } + versionedModule := fmt.Sprintf("%s@%s", libCommon, version) + path := filepath.Join(build.Default.GOPATH, "pkg", "mod", versionedModule, "dataplane_crds") + + if runtime.GOOS != "darwin" { + path, err = encodePath(path) + if err != nil { + return path, err + } + } + + return path, nil +} + // GetOpenShiftCRDDir returns the absolute path of the directory holding the // OpenShift custom resource definition. It will look the CRD path up from // lib-common module. diff --git a/modules/test/dataplane_crds/dataplane.openstack.org_openstackdataplanenodesets.yaml b/modules/test/dataplane_crds/dataplane.openstack.org_openstackdataplanenodesets.yaml new file mode 100644 index 00000000..c2c6b691 --- /dev/null +++ b/modules/test/dataplane_crds/dataplane.openstack.org_openstackdataplanenodesets.yaml @@ -0,0 +1,41 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: openstackdataplanenodesets.dataplane.openstack.org +spec: + group: dataplane.openstack.org + names: + kind: OpenStackDataPlaneNodeSet + listKind: OpenStackDataPlaneNodeSetList + plural: openstackdataplanenodesets + shortNames: + - osdpns + singular: openstackdataplanenodeset + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + type: object + x-kubernetes-preserve-unknown-fields: true + status: + properties: + secretHashes: + additionalProperties: + type: string + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} From 979888738e7fd03a593b3963bd3d1dbb5fb8af3b Mon Sep 17 00:00:00 2001 From: Luca Miccini Date: Tue, 12 May 2026 11:38:30 +0200 Subject: [PATCH 3/3] Add test for NoMatchError when NodeSet CRD is not installed Co-Authored-By: Claude Opus 4.6 --- modules/edpm/nodeset_test.go | 22 ++++++++++++++++++++++ modules/test/crd.go | 19 ++++++++++++++----- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/modules/edpm/nodeset_test.go b/modules/edpm/nodeset_test.go index 4b085fe6..1c7dbef0 100644 --- a/modules/edpm/nodeset_test.go +++ b/modules/edpm/nodeset_test.go @@ -61,6 +61,28 @@ func newTestSchemeAndMapper() (*runtime.Scheme, meta.RESTMapper) { return s, mapper } +func TestAreSecretHashesInSync_CRDNotInstalled(t *testing.T) { + s := runtime.NewScheme() + _ = corev1.AddToScheme(s) + + // Empty mapper — NodeSet GVK is not registered, simulating a cluster + // where the OpenStackDataPlaneNodeSet CRD is not installed. + mapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{}) + + c := fake.NewClientBuilder(). + WithScheme(s). + WithRESTMapper(mapper). + Build() + + inSync, info, err := AreSecretHashesInSync(context.Background(), c, "test") + if err != nil { + t.Errorf("AreSecretHashesInSync() unexpected error: %v", err) + } + if !inSync { + t.Errorf("AreSecretHashesInSync() inSync = false, want true when CRD not installed (info: %s)", info) + } +} + func TestAreSecretHashesInSync(t *testing.T) { currentSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ diff --git a/modules/test/crd.go b/modules/test/crd.go index dcd5c1ec..9070bd76 100644 --- a/modules/test/crd.go +++ b/modules/test/crd.go @@ -133,17 +133,26 @@ func GetCRDDirFromModule(moduleName string, goModPath string, relativeCRDPath st return path, nil } -// GetDataPlaneCRDDir returns the absolute path of the directory holding the -// dataplane custom resource definitions. It will look the CRD path up from -// the lib-common test module, similar to GetOpenShiftCRDDir. +// GetDataPlaneCRDDir returns the absolute path of the directory holding a +// minimal dataplane CRD shipped with lib-common. Use this only when the full +// OpenStackDataPlaneNodeSet CRD from openstack-operator is not available. +// It will look the CRD path up from the lib-common test module, similar to +// GetOpenShiftCRDDir. func GetDataPlaneCRDDir(goModPath string) (string, error) { libCommon := "github.com/openstack-k8s-operators/lib-common/modules/test" libCommon, version, err := getDependencyVersion(libCommon, goModPath) if err != nil { return "", err } - versionedModule := fmt.Sprintf("%s@%s", libCommon, version) - path := filepath.Join(build.Default.GOPATH, "pkg", "mod", versionedModule, "dataplane_crds") + + var path string + if version == "" && strings.HasPrefix(libCommon, ".") { + goModDir := filepath.Dir(goModPath) + path = filepath.Join(goModDir, libCommon, "dataplane_crds") + } else { + versionedModule := fmt.Sprintf("%s@%s", libCommon, version) + path = filepath.Join(build.Default.GOPATH, "pkg", "mod", versionedModule, "dataplane_crds") + } if runtime.GOOS != "darwin" { path, err = encodePath(path)