Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -85,17 +85,17 @@ 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
golang.org/x/text v0.39.0 // indirect
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
Expand Down
24 changes: 12 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
3 changes: 2 additions & 1 deletion internal/controller/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
19 changes: 14 additions & 5 deletions internal/controller/postgres_reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
161 changes: 161 additions & 0 deletions internal/controller/postgres_reconciler_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading