From e43d6922a2b7580cd1e9befdf3b1dc2c558bdb8d Mon Sep 17 00:00:00 2001 From: Lucas Alvares Gomes Date: Tue, 28 Jul 2026 08:54:42 +0100 Subject: [PATCH 1/2] Fix google.golang.org/grpc vulnerability GO-2026-6061 Bumps grpc from v1.78.0 to v1.82.1 to address vulnerabilities in the xDS RBAC authorization engine and HTTP/2 transport server implementation. Signed-off-by: Lucas Alvares Gomes --- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index b416d29..75160fb 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/openstack-k8s-operators/lib-common/modules/common v0.6.0 github.com/operator-framework/api v0.37.0 k8s.io/api v0.34.2 - k8s.io/apiextensions-apiserver v0.34.2 + k8s.io/apiextensions-apiserver v0.34.2 // indirect k8s.io/apimachinery v0.34.3 k8s.io/client-go v0.34.2 sigs.k8s.io/controller-runtime v0.22.4 @@ -25,7 +25,7 @@ require ( replace github.com/openshift/api => github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e require ( - cel.dev/expr v0.24.0 // indirect + cel.dev/expr v0.25.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect @@ -85,7 +85,7 @@ require ( golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/mod v0.37.0 // indirect golang.org/x/net v0.56.0 // indirect - golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/term v0.44.0 // indirect @@ -93,9 +93,9 @@ require ( golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.47.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect - google.golang.org/grpc v1.78.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/grpc v1.82.1 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 2c3b091..076c28c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= 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/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= @@ -208,8 +208,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= 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= @@ -241,14 +241,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T 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= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= -google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 0578ab2926a2320d8a1ec717c9d9b791b2f4c973 Mon Sep 17 00:00:00 2001 From: Lucas Alvares Gomes Date: Wed, 15 Jul 2026 16:55:35 +0100 Subject: [PATCH 2/2] Allow Postgres PVC expansion Previously, any change to spec.database.size on an existing OpenStackLightspeed instance returned ErrPostgresPVCSizeMismatch, forcing users to delete the PVC (and lose all data) to resize storage. Now the operator distinguishes three cases: - Expand: patches the PVC's storage request to the larger value. - Equal: no-op, no error. - Shrink: returns ErrPostgresPVCSizeShrink with a clear message showing the current and requested sizes; the PVC is not modified. Unit tests cover all three cases using a fake client. Kuttl tests was considered by rejected because it requires the cluster's default StorageClass to have allowVolumeExpansionenabled, which can not be assumed in a portable test environment. And since kuttl also has no proper mechanism to skip tests conditionally it would produce unreliable results across different clusters. Signed-off-by: Lucas Alvares Gomes --- internal/controller/errors.go | 3 +- internal/controller/postgres_reconciler.go | 19 ++- .../controller/postgres_reconciler_test.go | 161 ++++++++++++++++++ 3 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 internal/controller/postgres_reconciler_test.go diff --git a/internal/controller/errors.go b/internal/controller/errors.go index a3883a7..5f97677 100644 --- a/internal/controller/errors.go +++ b/internal/controller/errors.go @@ -67,6 +67,7 @@ var ( ErrGetPostgresConfigMap = errors.New("failed to get Postgres configmap") ErrCreatePostgresNetworkPolicy = errors.New("failed to create Postgres network policy") ErrCreatePostgresPVC = errors.New("failed to create Postgres PVC") + ErrPatchPostgresPVC = errors.New("failed to patch Postgres PVC") ErrGetPostgresPVC = errors.New("failed to get Postgres PVC") - ErrPostgresPVCSizeMismatch = errors.New("existing Postgres PVC size does not match requested size") + ErrPostgresPVCSizeShrink = errors.New("cannot shrink existing Postgres PVC") ) diff --git a/internal/controller/postgres_reconciler.go b/internal/controller/postgres_reconciler.go index 5adfb93..6d20fd7 100644 --- a/internal/controller/postgres_reconciler.go +++ b/internal/controller/postgres_reconciler.go @@ -248,13 +248,22 @@ func reconcilePostgresPVC(h *common_helper.Helper, ctx context.Context, instance } if err == nil { - // PVC already exists, validate if the size matches existingQty := pvc.Spec.Resources.Requests[corev1.ResourceStorage] - if requestedQty.Cmp(existingQty) != 0 { - return fmt.Errorf("%w: requested size %s but existing PVC has %s", - ErrPostgresPVCSizeMismatch, requestedQty.String(), existingQty.String()) + cmp := requestedQty.Cmp(existingQty) + if cmp < 0 { + return fmt.Errorf("%w: current %s, requested %s", + ErrPostgresPVCSizeShrink, existingQty.String(), requestedQty.String()) } - h.GetLogger().Info("Reusing the existing PostgreSQL PVC with a matching size", "name", pvc.Name) + if cmp == 0 { + h.GetLogger().Info("Reusing the existing PostgreSQL PVC with a matching size", "name", pvc.Name) + return nil + } + patch := client.MergeFrom(pvc.DeepCopy()) + pvc.Spec.Resources.Requests[corev1.ResourceStorage] = requestedQty + if err := h.GetClient().Patch(ctx, pvc, patch); err != nil { + return fmt.Errorf("%w: %w", ErrPatchPostgresPVC, err) + } + h.GetLogger().Info("Postgres PVC storage request expanded", "name", pvc.Name, "from", existingQty.String(), "to", requestedQty.String()) return nil } diff --git a/internal/controller/postgres_reconciler_test.go b/internal/controller/postgres_reconciler_test.go new file mode 100644 index 0000000..3e183c1 --- /dev/null +++ b/internal/controller/postgres_reconciler_test.go @@ -0,0 +1,161 @@ +/* +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 controller + +import ( + "bytes" + "context" + "errors" + "fmt" + "testing" + + common_helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + apiv1beta1 "github.com/openstack-k8s-operators/lightspeed-operator/api/v1beta1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +func makeExistingPVC(size string) *corev1.PersistentVolumeClaim { + return &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: PostgresDataPVCName, + Namespace: "test-ns", + }, + Spec: corev1.PersistentVolumeClaimSpec{ + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse(size), + }, + }, + }, + } +} + +func makeInstanceWithDBSize(size string) *apiv1beta1.OpenStackLightspeed { + return &apiv1beta1.OpenStackLightspeed{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-instance", + Namespace: "test-ns", + }, + Spec: apiv1beta1.OpenStackLightspeedSpec{ + Database: &apiv1beta1.DatabaseSpec{ + Size: resource.MustParse(size), + }, + }, + } +} + +// Expanding the PVC patches its storage request to the larger value and returns no error. +func TestReconcilePostgresPVC_Expand(t *testing.T) { + h := newTestHelper(t, makeExistingPVC("1Gi")) + instance := makeInstanceWithDBSize("2Gi") + + ctx := context.Background() + if err := reconcilePostgresPVC(h, ctx, instance); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + updated := &corev1.PersistentVolumeClaim{} + if err := h.GetClient().Get(ctx, client.ObjectKey{Name: PostgresDataPVCName, Namespace: "test-ns"}, updated); err != nil { + t.Fatalf("failed to get PVC after expand: %v", err) + } + got := updated.Spec.Resources.Requests[corev1.ResourceStorage] + want := resource.MustParse("2Gi") + if got.Cmp(want) != 0 { + t.Errorf("expected PVC storage %s after expand, got %s", want.String(), got.String()) + } +} + +// Shrinking the PVC is rejected with ErrPostgresPVCSizeShrink; the PVC is not modified. +func TestReconcilePostgresPVC_Shrink(t *testing.T) { + h := newTestHelper(t, makeExistingPVC("2Gi")) + instance := makeInstanceWithDBSize("1Gi") + + ctx := context.Background() + err := reconcilePostgresPVC(h, ctx, instance) + if err == nil { + t.Fatal("expected error when shrinking PVC, got nil") + } + if !errors.Is(err, ErrPostgresPVCSizeShrink) { + t.Errorf("expected ErrPostgresPVCSizeShrink, got %v", err) + } +} + +// When the requested size matches the existing PVC, reconciliation is a no-op. +func TestReconcilePostgresPVC_NoOp(t *testing.T) { + h := newTestHelper(t, makeExistingPVC("1Gi")) + instance := makeInstanceWithDBSize("1Gi") + + ctx := context.Background() + if err := reconcilePostgresPVC(h, ctx, instance); err != nil { + t.Fatalf("unexpected error for matching size: %v", err) + } +} + +func newTestHelperWithPatchErr(t *testing.T, objs ...client.Object) *common_helper.Helper { + t.Helper() + + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatalf("failed to add corev1 to scheme: %v", err) + } + if err := apiv1beta1.AddToScheme(scheme); err != nil { + t.Fatalf("failed to add apiv1beta1 to scheme: %v", err) + } + + patchErr := fmt.Errorf("forced patch failure") + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(objs...). + WithInterceptorFuncs(interceptor.Funcs{ + Patch: func(_ context.Context, _ client.WithWatch, _ client.Object, _ client.Patch, _ ...client.PatchOption) error { + return patchErr + }, + }). + Build() + + instance := &apiv1beta1.OpenStackLightspeed{ + ObjectMeta: metav1.ObjectMeta{Name: "test-instance", Namespace: "test-ns"}, + } + logger := zap.New(zap.WriteTo(bytes.NewBuffer(nil))) + h, err := common_helper.NewHelper(instance, fakeClient, nil, scheme, logger) + if err != nil { + t.Fatalf("failed to create helper: %v", err) + } + return h +} + +// If the API server rejects the patch (e.g. StorageClass lacks allowVolumeExpansion), ErrPatchPostgresPVC is returned. +func TestReconcilePostgresPVC_PatchError(t *testing.T) { + h := newTestHelperWithPatchErr(t, makeExistingPVC("1Gi")) + instance := makeInstanceWithDBSize("2Gi") + + ctx := context.Background() + err := reconcilePostgresPVC(h, ctx, instance) + if err == nil { + t.Fatal("expected error when patch fails, got nil") + } + if !errors.Is(err, ErrPatchPostgresPVC) { + t.Errorf("expected ErrPatchPostgresPVC, got %v", err) + } +}