Repository: openshift/lightspeed-operator
Pull request #728: OLS-1686: OLS should persist postgres data
Author: login: syedriko
Labels: name: jira/valid-reference
description: Indicates that this PR references a valid Jira ticket of any type.
name: lgtm
description: Indicates that a PR is ready to be merged.
name: approved
description: Indicates a PR has been approved by an approver from all required OWNERS files.
Created: 2025-05-05T03:07:30Z | Merged: 2025-05-15T22:06:06Z

## Description

Added a PVC for the Postgres data volume

## Type of change

- [ ] Refactor
- [x] New feature
- [ ] Bug fix
- [ ] CVE fix
- [ ] Optimization
- [ ] Documentation Update
- [ ] Configuration Update
- [ ] Bump-up dependent library

## Related Tickets & Documents

- Related Issue #
- Closes # [OLS-1686](https://issues.redhat.com//browse/OLS-1686)

## Checklist before requesting a review

- [x] I have performed a self-review of my code.
- [ ] PR has passed all pre-merge test jobs.
- [ ] If it is a core feature, I have added thorough tests.

## Testing
- Please provide detailed steps to perform tests related to this code change.
- How were the fix/results from this change verified? Please provide relevant screenshots or results.



Diff:
diff --git a/api/v1alpha1/olsconfig_types.go b/api/v1alpha1/olsconfig_types.go
index 57fb5b04d..2dfec6d2b 100644
--- a/api/v1alpha1/olsconfig_types.go
+++ b/api/v1alpha1/olsconfig_types.go
@@ -22,9 +22,6 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
-// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.
-
 // OLSConfigSpec defines the desired state of OLSConfig
 type OLSConfigSpec struct {
 	// +kubebuilder:validation:Required
@@ -101,6 +98,20 @@ type OLSSpec struct {
 	// LLM Token Quota Configuration
 	// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="LLM Token Quota Configuration"
 	QuotaHandlersConfig *QuotaHandlersConfig `json:"quotaHandlersConfig,omitempty"`
+	// Persistent Storage Configuration
+	// +kubebuilder:validation:Optional
+	// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Persistent Storage Configuration",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced"}
+	Storage *Storage `json:"storage,omitempty"`
+}
+
+// Persistent Storage Configuration
+type Storage struct {
+	// Size of the Requested Volume
+	// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Size of the Requested Volume"
+	Size string `json:"size,omitempty"`
+	// Storage Class of the Requested Volume
+	// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Storage Class of the Requested Volume"
+	Class string `json:"class,omitempty"`
 }
 
 // RAGSpec defines how to retrieve a RAG databases.
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index a9f9d9206..64ed7e561 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -377,6 +377,11 @@ func (in *OLSSpec) DeepCopyInto(out *OLSSpec) {
 		*out = new(QuotaHandlersConfig)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Storage != nil {
+		in, out := &in.Storage, &out.Storage
+		*out = new(Storage)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OLSSpec.
@@ -480,6 +485,21 @@ func (in *RAGSpec) DeepCopy() *RAGSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Storage) DeepCopyInto(out *Storage) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Storage.
+func (in *Storage) DeepCopy() *Storage {
+	if in == nil {
+		return nil
+	}
+	out := new(Storage)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *TLSConfig) DeepCopyInto(out *TLSConfig) {
 	*out = *in
diff --git a/bundle/manifests/lightspeed-operator.clusterserviceversion.yaml b/bundle/manifests/lightspeed-operator.clusterserviceversion.yaml
index b5645104d..ff399950a 100644
--- a/bundle/manifests/lightspeed-operator.clusterserviceversion.yaml
+++ b/bundle/manifests/lightspeed-operator.clusterserviceversion.yaml
@@ -512,6 +512,7 @@ spec:
                 - configmaps
                 - serviceaccounts
                 - services
+                - persistentvolumeclaims
               verbs:
                 - create
                 - delete
diff --git a/config/crd/bases/ols.openshift.io_olsconfigs.yaml b/config/crd/bases/ols.openshift.io_olsconfigs.yaml
index 31d0bdeb8..485873d3d 100644
--- a/config/crd/bases/ols.openshift.io_olsconfigs.yaml
+++ b/config/crd/bases/ols.openshift.io_olsconfigs.yaml
@@ -787,6 +787,16 @@ spec:
                       - indexPath
                       type: object
                     type: array
+                  storage:
+                    description: Persistent Storage Configuration
+                    properties:
+                      class:
+                        description: Storage Class of the Requested Volume
+                        type: string
+                      size:
+                        description: Size of the Requested Volume
+                        type: string
+                    type: object
                   tlsConfig:
                     description: TLS configuration of the Lightspeed backend's HTTPS
                       endpoint
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 697d98211..4a95e5fec 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -106,6 +106,14 @@ rules:
   - create
   - list
   - watch
+- apiGroups:
+  - storage.k8s.io
+  resources:
+  - storageclasses
+  verbs:
+  - get
+  - list
+  - watch
 ---
 apiVersion: rbac.authorization.k8s.io/v1
 kind: Role
@@ -117,6 +125,7 @@ rules:
   - ""
   resources:
   - configmaps
+  - persistentvolumeclaims
   - serviceaccounts
   - services
   verbs:
diff --git a/docs/olsconfig-ols-openshift-io-v1alpha1.adoc b/docs/olsconfig-ols-openshift-io-v1alpha1.adoc
index 917360ab5..d2fe671c7 100644
--- a/docs/olsconfig-ols-openshift-io-v1alpha1.adoc
+++ b/docs/olsconfig-ols-openshift-io-v1alpha1.adoc
@@ -511,6 +511,10 @@ Required::
 | `array`
 | RAG databases
 
+| `storage`
+| `object`
+| Persistent Storage Configuration
+
 | `tlsConfig`
 | `object`
 | TLS configuration of the Lightspeed backend's HTTPS endpoint
@@ -1307,6 +1311,32 @@ Required::
 | `string`
 | The path to the RAG database inside of the container image
 
+|===
+== .spec.ols.storage
+Description::
++
+--
+Persistent Storage Configuration
+--
+
+Type::
+  `object`
+
+
+
+
+[cols="1,1,1",options="header"]
+|===
+| Property | Type | Description
+
+| `class`
+| `string`
+| Storage Class of the Requested Volume
+
+| `size`
+| `string`
+| Size of the Requested Volume
+
 |===
 == .spec.ols.tlsConfig
 Description::
diff --git a/internal/controller/constants.go b/internal/controller/constants.go
index be1c209be..d46bc695f 100644
--- a/internal/controller/constants.go
+++ b/internal/controller/constants.go
@@ -135,51 +135,51 @@ const (
 	WatcherAnnotationKey = "ols.openshift.io/watcher"
 
 	/*** Postgres Constants ***/
-	// PostgresCAVolume is the name of the OLS postgres TLS ca certificate volume name
+	// PostgresCAVolume is the name of the OLS Postgres TLS ca certificate volume name
 	PostgresCAVolume = "cm-olspostgresca"
-	// PostgresDeploymentName is the name of OLS application postgres deployment
+	// PostgresDeploymentName is the name of OLS application Postgres deployment
 	PostgresDeploymentName = "lightspeed-postgres-server"
-	// PostgresSecretKeyName is the name of the key holding postgres server secret
+	// PostgresSecretKeyName is the name of the key holding Postgres server secret
 	PostgresSecretKeyName = "password"
 	// Image of the OLS application postgres server
 	PostgresServerImageDefault = "registry.redhat.io/rhel9/postgresql-16@sha256:6d2cab6cb6366b26fcf4591fe22aa5e8212a3836c34c896bb65977a8e50d658b"
 	// PostgresDefaultUser is the default user name for postgres
 	PostgresDefaultUser = "postgres"
-	// PostgresDefaultDbName is the default db name for postgres
+	// PostgresDefaultDbName is the default db name for Postgres
 	PostgresDefaultDbName = "postgres"
-	// PostgresConfigHashKey is the key of the hash value of the OLS's postgres config
+	// PostgresConfigHashKey is the key of the hash value of the OLS's Postgres config
 	PostgresConfigHashKey = "hash/olspostgresconfig"
 	// PostgresSecretHashKey is the key of the hash value of OLS Postgres secret
 	// #nosec G101
 	PostgresSecretHashKey = "hash/postgres-secret"
-	// PostgresServiceName is the name of OLS application postgres server service
+	// PostgresServiceName is the name of OLS application Postgres server service
 	PostgresServiceName = "lightspeed-postgres-server"
-	// PostgresSecretName is the name of OLS application postgres secret
+	// PostgresSecretName is the name of OLS application Postgres secret
 	PostgresSecretName = "lightspeed-postgres-secret"
-	// PostgresCertsSecretName is the name of the postgres certs secret
+	// PostgresCertsSecretName is the name of the Postgres certs secret
 	PostgresCertsSecretName = "lightspeed-postgres-certs"
-	// PostgresBootstrapSecretName is the name of the postgres bootstrap secret
+	// PostgresBootstrapSecretName is the name of the Postgres bootstrap secret
 	// #nosec G101
 	PostgresBootstrapSecretName = "lightspeed-postgres-bootstrap"
 	// PostgresBootstrapVolumeMountPath is the path of bootstrap volume mount
 	PostgresBootstrapVolumeMountPath = "/usr/share/container-scripts/postgresql/start/create-extensions.sh"
-	// PostgresExtensionScript is the name of the postgres extensions script
+	// PostgresExtensionScript is the name of the Postgres extensions script
 	PostgresExtensionScript = "create-extensions.sh"
-	// PostgresConfigMap is the name of the postgres config map
+	// PostgresConfigMap is the name of the Postgres config map
 	PostgresConfigMap = "lightspeed-postgres-conf"
-	// PostgresConfigVolumeMountPath is the path of postgres configuration volume mount
+	// PostgresConfigVolumeMountPath is the path of Postgres configuration volume mount
 	PostgresConfigVolumeMountPath = "/usr/share/pgsql/postgresql.conf.sample"
-	// PostgresConfig is the name of postgres configuration used to start the server
+	// PostgresConfig is the name of Postgres configuration used to start the server
 	PostgresConfig = "postgresql.conf.sample"
-	// PostgresDataVolume is the name of postgres data volume
+	// PostgresDataVolume is the name of Postgres data volume
 	PostgresDataVolume = "postgres-data"
-	// PostgresDataVolumeMountPath is the path of postgres data volume mount
+	// PostgresDataVolumeMountPath is the path of Postgres data volume mount
 	PostgresDataVolumeMountPath = "/var/lib/pgsql/data"
-	// PostgresServicePort is the port number of the OLS postgres server service
+	// PostgresServicePort is the port number of the OLS Postgres server service
 	PostgresServicePort = 5432
-	// PostgresSharedBuffers is the share buffers value for postgres cache
+	// PostgresSharedBuffers is the share buffers value for Postgres cache
 	PostgresSharedBuffers = "256MB"
-	// PostgresMaxConnections is the max connections values for postgres cache
+	// PostgresMaxConnections is the max connections values for Postgres cache
 	PostgresMaxConnections = 2000
 	// PostgresDefaultSSLMode is the default ssl mode for postgres
 	PostgresDefaultSSLMode = "require"
@@ -206,6 +206,12 @@ ssl_ca_file = '/etc/certs/cm-olspostgresca/service-ca.crt'
 	// PostgresNetworkPolicyName is the name of the network policy for the OLS postgres server
 	PostgresNetworkPolicyName = "lightspeed-postgres-server"
 
+	// PostgresPVCName is the name of the PVC for the OLS Postgres server
+	PostgresPVCName = "lightspeed-postgres-pvc"
+
+	// PostgresDefaultPVCSize is the default size of the PVC for the OLS Postgres server
+	PostgresDefaultPVCSize = "1Gi"
+
 	/*** state cache keys ***/
 	// OLSAppTLSHashStateCacheKey is the key of the hash value of the OLS App TLS certificates
 	OLSAppTLSHashStateCacheKey = "olsapptls-hash"
diff --git a/internal/controller/errors.go b/internal/controller/errors.go
index 8c72154f7..f09c5a92e 100644
--- a/internal/controller/errors.go
+++ b/internal/controller/errors.go
@@ -22,6 +22,7 @@ const (
 	ErrCreatePostgresBootstrapSecret      = "failed to create OLS Postgres bootstrap secret"
 	ErrCreatePostgresConfigMap            = "failed to create OLS Postgres configmap"
 	ErrCreatePostgresService              = "failed to create OLS Postgres service"
+	ErrCreatePostgresPVC                  = "failed to create OLS Postgres PVC"
 	ErrCreatePostgresDeployment           = "failed to create OLS Postgres deployment"
 	ErrCreatePostgresNetworkPolicy        = "failed to create OLS Postgres network policy"
 	ErrDeleteConsolePlugin                = "failed to delete Console Plugin"
@@ -48,6 +49,7 @@ const (
 	ErrGeneratePostgresSecret             = "failed to generate OLS Postgres secret"
 	ErrGeneratePostgresBootstrapSecret    = "failed to generate OLS Postgres bootstrap secret"
 	ErrGeneratePostgresConfigMap          = "failed to generate OLS Postgres configmap"
+	ErrGeneratePostgresPVC                = "failed to generate OLS Postgres PVC"
 	ErrGeneratePostgresService            = "failed to generate OLS Postgres service"
 	ErrGeneratePostgresSecretHash         = "failed to generate hash for the existing OLS postgres secret"
 	ErrGeneratePostgresDeployment         = "failed to generate OLS Postgres deployment"
@@ -93,6 +95,7 @@ const (
 	ErrGetPostgresBootstrapSecret = "failed to get OLS Postgres bootstrap secret"
 	ErrGetPostgresConfigMap       = "failed to get OLS Postgres configmap"
 	ErrGetPostgresService         = "failed to get OLS Postgres service"
+	ErrGetPostgresPVC             = "failed to get OLS Postgres PVC"
 	ErrGetPostgresDeployment      = "failed to get OLS Postgres deployment"
 	ErrUpdateProviderSecret       = "failed to update provider secret"
 	ErrUpdatePostgresSecret       = "failed to update OLS Postgres secret"
diff --git a/internal/controller/ols_app_postgres_assets.go b/internal/controller/ols_app_postgres_assets.go
index e60117727..2f4929261 100644
--- a/internal/controller/ols_app_postgres_assets.go
+++ b/internal/controller/ols_app_postgres_assets.go
@@ -11,6 +11,7 @@ import (
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	networkingv1 "k8s.io/api/networking/v1"
+	storagev1 "k8s.io/api/storage/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/intstr"
@@ -59,7 +60,7 @@ func (r *OLSConfigReconciler) generatePostgresDeployment(cr *olsv1alpha1.OLSConf
 
 	passwordMap, err := getSecretContent(r.Client, postgresSecretName, r.Options.Namespace, []string{OLSComponentPasswordFileName}, &corev1.Secret{})
 	if err != nil {
-		return nil, fmt.Errorf("Password is a must to start postgres deployment : %w", err)
+		return nil, fmt.Errorf("password is needed to start postgres deployment : %w", err)
 	}
 	postgresPassword := passwordMap[OLSComponentPasswordFileName]
 	if cr.Spec.OLSConfig.ConversationCache.Postgres.SharedBuffers == "" {
@@ -94,12 +95,22 @@ func (r *OLSConfigReconciler) generatePostgresDeployment(cr *olsv1alpha1.OLSConf
 			},
 		},
 	}
+
 	dataVolume := corev1.Volume{
 		Name: PostgresDataVolume,
-		VolumeSource: corev1.VolumeSource{
+	}
+	if cr.Spec.OLSConfig.Storage != nil {
+		dataVolume.VolumeSource = corev1.VolumeSource{
+			PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+				ClaimName: PostgresPVCName,
+			},
+		}
+	} else {
+		dataVolume.VolumeSource = corev1.VolumeSource{
 			EmptyDir: &corev1.EmptyDirVolumeSource{},
-		},
+		}
 	}
+
 	volumes := []corev1.Volume{tlsCertsVolume, bootstrapVolume, configVolume, dataVolume, getPostgresCAConfigVolume()}
 	postgresTLSVolumeMount := corev1.VolumeMount{
 		Name:      "secret-" + PostgresCertsSecretName,
@@ -380,3 +391,55 @@ func (r *OLSConfigReconciler) generatePostgresNetworkPolicy(cr *olsv1alpha1.OLSC
 	}
 	return &np, nil
 }
+
+func (r *OLSConfigReconciler) storageDefaults(s *olsv1alpha1.Storage) error {
+	if s.Size == "" {
+		s.Size = PostgresDefaultPVCSize
+	}
+	if s.Class == "" {
+		var scList storagev1.StorageClassList
+		ctx := context.Background()
+		if err := r.List(ctx, &scList); err == nil {
+			for _, sc := range scList.Items {
+				if sc.Annotations["storageclass.kubernetes.io/is-default-class"] == "true" {
+					s.Class = sc.Name
+				}
+			}
+		}
+		if s.Class == "" {
+			return fmt.Errorf("no storage class specified and no default storage class configured")
+		}
+	}
+	return nil
+}
+
+func (r *OLSConfigReconciler) generatePostgresPVC(cr *olsv1alpha1.OLSConfig) (*corev1.PersistentVolumeClaim, error) {
+
+	storage := cr.Spec.OLSConfig.Storage
+	if err := r.storageDefaults(storage); err != nil {
+		return nil, err
+	}
+
+	pvc := &corev1.PersistentVolumeClaim{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      PostgresPVCName,
+			Namespace: r.Options.Namespace,
+		},
+		Spec: corev1.PersistentVolumeClaimSpec{
+			AccessModes: []corev1.PersistentVolumeAccessMode{
+				corev1.PersistentVolumeAccessMode("ReadWriteOnce"),
+			},
+			Resources: corev1.VolumeResourceRequirements{
+				Requests: corev1.ResourceList{
+					corev1.ResourceStorage: resource.MustParse(storage.Size),
+				},
+			},
+			StorageClassName: &storage.Class,
+		},
+	}
+
+	if err := controllerutil.SetControllerReference(cr, pvc, r.Scheme); err != nil {
+		return nil, err
+	}
+	return pvc, nil
+}
diff --git a/internal/controller/ols_app_postgres_assets_test.go b/internal/controller/ols_app_postgres_assets_test.go
index dbc5e2f99..a49f229cd 100644
--- a/internal/controller/ols_app_postgres_assets_test.go
+++ b/internal/controller/ols_app_postgres_assets_test.go
@@ -23,7 +23,7 @@ var _ = Describe("App postgres server assets", func() {
 	var r *OLSConfigReconciler
 	var rOptions *OLSConfigReconcilerOptions
 
-	validatePostgresDeployment := func(dep *appsv1.Deployment, password string) {
+	validatePostgresDeployment := func(dep *appsv1.Deployment, password string, with_pvc bool) {
 		replicas := int32(1)
 		revisionHistoryLimit := int32(1)
 		defaultPermission := int32(0600)
@@ -101,7 +101,7 @@ var _ = Describe("App postgres server assets", func() {
 				ReadOnly:  true,
 			},
 		}))
-		Expect(dep.Spec.Template.Spec.Volumes).To(Equal([]corev1.Volume{
+		expectedVolumes := []corev1.Volume{
 			{
 				Name: "secret-" + PostgresCertsSecretName,
 				VolumeSource: corev1.VolumeSource{
@@ -127,21 +127,33 @@ var _ = Describe("App postgres server assets", func() {
 					},
 				},
 			},
-			{
+		}
+		if with_pvc {
+			expectedVolumes = append(expectedVolumes, corev1.Volume{
 				Name: PostgresDataVolume,
 				VolumeSource: corev1.VolumeSource{
-					EmptyDir: &corev1.EmptyDirVolumeSource{},
+					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+						ClaimName: PostgresPVCName,
+					},
 				},
-			},
-			{
-				Name: PostgresCAVolume,
+			})
+		} else {
+			expectedVolumes = append(expectedVolumes, corev1.Volume{
+				Name: PostgresDataVolume,
 				VolumeSource: corev1.VolumeSource{
-					ConfigMap: &corev1.ConfigMapVolumeSource{
-						LocalObjectReference: corev1.LocalObjectReference{Name: OLSCAConfigMap},
-					},
+					EmptyDir: &corev1.EmptyDirVolumeSource{},
+				},
+			})
+		}
+		expectedVolumes = append(expectedVolumes, corev1.Volume{
+			Name: PostgresCAVolume,
+			VolumeSource: corev1.VolumeSource{
+				ConfigMap: &corev1.ConfigMapVolumeSource{
+					LocalObjectReference: corev1.LocalObjectReference{Name: OLSCAConfigMap},
 				},
 			},
-		}))
+		})
+		Expect(dep.Spec.Template.Spec.Volumes).To(Equal(expectedVolumes))
 	}
 
 	validatePostgresService := func(service *corev1.Service, err error) {
@@ -206,6 +218,31 @@ var _ = Describe("App postgres server assets", func() {
 		Expect(networkPolicy.Spec.PodSelector.MatchLabels).To(Equal(generatePostgresSelectorLabels()))
 	}
 
+	createAndValidatePostgresDeployment := func(with_pvc bool) {
+		if with_pvc {
+			cr.Spec.OLSConfig.Storage = &olsv1alpha1.Storage{}
+		}
+		cr.Spec.OLSConfig.ConversationCache.Postgres.CredentialsSecret = "dummy-secret-1"
+		secret, _ := r.generatePostgresSecret(cr)
+		secret.SetOwnerReferences([]metav1.OwnerReference{
+			{
+				Kind:       "Secret",
+				APIVersion: "v1",
+				UID:        "ownerUID",
+				Name:       "dummy-secret-1",
+			},
+		})
+		secretCreationErr := r.Create(ctx, secret)
+		Expect(secretCreationErr).NotTo(HaveOccurred())
+		passwordMap, _ := getSecretContent(r.Client, secret.Name, cr.Namespace, []string{OLSComponentPasswordFileName}, secret)
+		password := passwordMap[OLSComponentPasswordFileName]
+		deployment, err := r.generatePostgresDeployment(cr)
+		Expect(err).NotTo(HaveOccurred())
+		validatePostgresDeployment(deployment, password, with_pvc)
+		secretDeletionErr := r.Delete(ctx, secret)
+		Expect(secretDeletionErr).NotTo(HaveOccurred())
+	}
+
 	Context("complete custom resource", func() {
 		BeforeEach(func() {
 			rOptions = &OLSConfigReconcilerOptions{
@@ -223,25 +260,11 @@ var _ = Describe("App postgres server assets", func() {
 		})
 
 		It("should generate the OLS postgres deployment", func() {
-			cr.Spec.OLSConfig.ConversationCache.Postgres.CredentialsSecret = "dummy-secret-1"
-			secret, _ := r.generatePostgresSecret(cr)
-			secret.SetOwnerReferences([]metav1.OwnerReference{
-				{
-					Kind:       "Secret",
-					APIVersion: "v1",
-					UID:        "ownerUID",
-					Name:       "dummy-secret-1",
-				},
-			})
-			secretCreationErr := r.Create(ctx, secret)
-			Expect(secretCreationErr).NotTo(HaveOccurred())
-			passwordMap, _ := getSecretContent(r.Client, secret.Name, cr.Namespace, []string{OLSComponentPasswordFileName}, secret)
-			password := passwordMap[OLSComponentPasswordFileName]
-			deployment, err := r.generatePostgresDeployment(cr)
-			Expect(err).NotTo(HaveOccurred())
-			validatePostgresDeployment(deployment, password)
-			secretDeletionErr := r.Delete(ctx, secret)
-			Expect(secretDeletionErr).NotTo(HaveOccurred())
+			createAndValidatePostgresDeployment(false)
+		})
+
+		It("should generate the OLS postgres deployment with a PVC data volume", func() {
+			createAndValidatePostgresDeployment(true)
 		})
 
 		It("should work when no update in the OLS postgres deployment", func() {
@@ -394,7 +417,7 @@ var _ = Describe("App postgres server assets", func() {
 			password := passwordMap[OLSComponentPasswordFileName]
 			deployment, err := r.generatePostgresDeployment(cr)
 			Expect(err).NotTo(HaveOccurred())
-			validatePostgresDeployment(deployment, password)
+			validatePostgresDeployment(deployment, password, false)
 			secretDeletionErr := r.Delete(ctx, secret)
 			Expect(secretDeletionErr).NotTo(HaveOccurred())
 		})
diff --git a/internal/controller/ols_app_postgres_reconciliator.go b/internal/controller/ols_app_postgres_reconciliator.go
index 076a95ee9..3706b5e80 100644
--- a/internal/controller/ols_app_postgres_reconciliator.go
+++ b/internal/controller/ols_app_postgres_reconciliator.go
@@ -34,6 +34,10 @@ func (r *OLSConfigReconciler) reconcilePostgresServer(ctx context.Context, olsco
 			Name: "reconcile Postgres Service",
 			Task: r.reconcilePostgresService,
 		},
+		{
+			Name: "reconcile Postgres PVC",
+			Task: r.reconcilePostgresPVC,
+		},
 		{
 			Name: "reconcile Postgres Deployment",
 			Task: r.reconcilePostgresDeployment,
@@ -94,6 +98,30 @@ func (r *OLSConfigReconciler) reconcilePostgresDeployment(ctx context.Context, c
 	return nil
 }
 
+func (r *OLSConfigReconciler) reconcilePostgresPVC(ctx context.Context, cr *olsv1alpha1.OLSConfig) error {
+
+	if cr.Spec.OLSConfig.Storage == nil {
+		return nil
+	}
+	pvc, err := r.generatePostgresPVC(cr)
+	if err != nil {
+		return fmt.Errorf("%s: %w", ErrGeneratePostgresPVC, err)
+	}
+
+	foundPVC := &corev1.PersistentVolumeClaim{}
+	err = r.Client.Get(ctx, client.ObjectKey{Name: PostgresPVCName, Namespace: r.Options.Namespace}, foundPVC)
+	if err != nil && errors.IsNotFound(err) {
+		err = r.Create(ctx, pvc)
+		if err != nil {
+			return fmt.Errorf("%s: %w", ErrCreatePostgresPVC, err)
+		}
+	} else if err != nil {
+		return fmt.Errorf("%s: %w", ErrGetPostgresPVC, err)
+	}
+	r.logger.Info("OLS postgres PVC reconciled", "pvc", pvc.Name)
+	return nil
+}
+
 func (r *OLSConfigReconciler) reconcilePostgresService(ctx context.Context, cr *olsv1alpha1.OLSConfig) error {
 	service, err := r.generatePostgresService(cr)
 	if err != nil {
diff --git a/internal/controller/ols_app_postgres_reconciliator_test.go b/internal/controller/ols_app_postgres_reconciliator_test.go
index 3b3ba2bf3..77800766d 100644
--- a/internal/controller/ols_app_postgres_reconciliator_test.go
+++ b/internal/controller/ols_app_postgres_reconciliator_test.go
@@ -10,6 +10,7 @@ import (
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	networkingv1 "k8s.io/api/networking/v1"
+	storagev1 "k8s.io/api/storage/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 )
@@ -17,8 +18,8 @@ import (
 var _ = Describe("Postgres server reconciliator", Ordered, func() {
 
 	Context("Creation logic", Ordered, func() {
-		var secret *corev1.Secret
-		var bootstrapSecret *corev1.Secret
+		var secret, bootstrapSecret *corev1.Secret
+		var sc *storagev1.StorageClass
 		BeforeEach(func() {
 			By("create the provider secret")
 			secret, _ = generateRandomSecret()
@@ -61,9 +62,18 @@ var _ = Describe("Postgres server reconciliator", Ordered, func() {
 			})
 			bootstrapSecretCreationErr := reconciler.Create(ctx, bootstrapSecret)
 			Expect(bootstrapSecretCreationErr).NotTo(HaveOccurred())
+
+			By("Creating default StorageClass")
+			sc = buildDefaultStorageClass()
+			storageClassCreationErr := reconciler.Create(ctx, sc)
+			Expect(storageClassCreationErr).NotTo(HaveOccurred())
 		})
 
 		AfterEach(func() {
+			By("Deleting default StorageClass")
+			storageClassDeletionErr := reconciler.Delete(ctx, sc)
+			Expect(storageClassDeletionErr).NotTo(HaveOccurred())
+
 			By("Delete the provider secret")
 			secretDeletionErr := reconciler.Delete(ctx, secret)
 			Expect(secretDeletionErr).NotTo(HaveOccurred())
@@ -175,3 +185,20 @@ var _ = Describe("Postgres server reconciliator", Ordered, func() {
 		})
 	})
 })
+
+func buildDefaultStorageClass() *storagev1.StorageClass {
+	trueVal := true
+	immediate := storagev1.VolumeBindingImmediate
+
+	return &storagev1.StorageClass{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "standard",
+			Annotations: map[string]string{
+				"storageclass.kubernetes.io/is-default-class": "true",
+			},
+		},
+		Provisioner:          "kubernetes.io/no-provisioner",
+		AllowVolumeExpansion: &trueVal,
+		VolumeBindingMode:    &immediate,
+	}
+}
diff --git a/internal/controller/olsconfig_controller.go b/internal/controller/olsconfig_controller.go
index 5086182e7..bb074ce2b 100644
--- a/internal/controller/olsconfig_controller.go
+++ b/internal/controller/olsconfig_controller.go
@@ -70,23 +70,23 @@ type OLSConfigReconcilerOptions struct {
 // +kubebuilder:rbac:groups=ols.openshift.io,resources=olsconfigs/status,verbs=get;update;patch
 // +kubebuilder:rbac:groups=ols.openshift.io,resources=olsconfigs/finalizers,verbs=update
 // RBAC for managing deployments of OLS application server
-// +kubebuilder:rbac:groups=apps,namespace=openshift-lightspeed,resources=deployments,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
 // Service for exposing lightspeed service API endpoints
-// +kubebuilder:rbac:groups=core,namespace=openshift-lightspeed,resources=services,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete
 // ServiceAccount to run OLS application server
-// +kubebuilder:rbac:groups=core,namespace=openshift-lightspeed,resources=serviceaccounts,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch;create;update;patch;delete
 // ConfigMap for OLS application server configuration
-// +kubebuilder:rbac:groups=core,namespace=openshift-lightspeed,resources=configmaps,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete
 // ConfigMap in other namespaces:
 // - client CA certificate from openshift-monitoring namespace
 // OLM cannot create a role and rolebinding for a specific single namespace that is not the namespace the operator is installed in and/or watching
 // This has to be a cluster role
-// +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch
+// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch
 // Secret access for conversation cache server configuration
-// +kubebuilder:rbac:groups=core,namespace=openshift-lightspeed,resources=secrets,verbs=get;list;watch;create;update;patch;delete;deletecollection
+// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete;deletecollection
 // Secret access for telemetry pull secret, must be a cluster role due to OLM limitations in managing roles in operator namespace
-// +kubebuilder:rbac:groups=core,resources=secrets,verbs=list;watch
-// +kubebuilder:rbac:groups=core,resources=secrets,resourceNames=pull-secret,verbs=get;list;watch
+// +kubebuilder:rbac:groups="",resources=secrets,verbs=list;watch
+// +kubebuilder:rbac:groups="",resources=secrets,resourceNames=pull-secret,verbs=get;list;watch
 // ConsolePlugin for install console plugin
 // +kubebuilder:rbac:groups=console.openshift.io,resources=consolelinks;consoleexternalloglinks;consoleplugins;consoleplugins/finalizers,verbs=get;create;update;delete
 // Modify console CR to activate console plugin
@@ -99,16 +99,20 @@ type OLSConfigReconcilerOptions struct {
 // +kubebuilder:rbac:groups=authentication.k8s.io,resources=tokenreviews,verbs=create
 
 // ServiceMonitor for monitoring OLS application server
-// +kubebuilder:rbac:groups=monitoring.coreos.com,namespace=openshift-lightspeed,resources=servicemonitors,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors,verbs=get;list;watch;create;update;patch;delete
 
 // PrometheusRule for aggregating OLS metrics for telemetry
-// +kubebuilder:rbac:groups=monitoring.coreos.com,namespace=openshift-lightspeed,resources=prometheusrules,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=monitoring.coreos.com,resources=prometheusrules,verbs=get;list;watch;create;update;patch;delete
 
 // clusterversion for checking the openshift cluster version
 // +kubebuilder:rbac:groups=config.openshift.io,resources=clusterversions;apiservers,verbs=get;list;watch
 
 // NetworkPolicy for restricting access to OLS pods
-// +kubebuilder:rbac:groups=networking.k8s.io,namespace=openshift-lightspeed,resources=networkpolicies,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=networking.k8s.io,resources=networkpolicies,verbs=get;list;watch;create;update;patch;delete
+
+// PVC access for the Postgres PVC
+// +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=storage.k8s.io,resources=storageclasses,verbs=get;list;watch
 
 // For more details, check Reconcile and its Result here:
 // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.3/pkg/reconcile
@@ -241,6 +245,7 @@ func (r *OLSConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
 		Owns(&corev1.Service{}).
 		Owns(&corev1.ConfigMap{}).
 		Owns(&corev1.Secret{}).
+		Owns(&corev1.PersistentVolumeClaim{}).
 		Watches(&corev1.Secret{}, handler.EnqueueRequestsFromMapFunc(secretWatcherFilter)).
 		Watches(&corev1.Secret{}, handler.EnqueueRequestsFromMapFunc(telemetryPullSecretWatcherFilter)).
 		Watches(&corev1.ConfigMap{}, handler.EnqueueRequestsFromMapFunc(configMapWatcherFilter)).


Reviews:
user: login: raptorsun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T07:56:33Z
user: login: syedriko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T16:41:31Z
user: login: xrajesh
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T03:34:16Z
user: login: syedriko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T03:49:45Z

Review comments:
diff_hunk: @@ -131,6 +131,9 @@ var _ = Describe("App postgres server assets", func() {
 				Name: PostgresDataVolume,
 				VolumeSource: corev1.VolumeSource{
 					EmptyDir: &corev1.EmptyDirVolumeSource{},
+					//					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
path: internal/controller/ols_app_postgres_assets_test.go
user: login: raptorsun
body: let's add a test case to generate the PVC,too. 
created_at: 2025-05-12T07:56:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -131,6 +131,9 @@ var _ = Describe("App postgres server assets", func() {
 				Name: PostgresDataVolume,
 				VolumeSource: corev1.VolumeSource{
 					EmptyDir: &corev1.EmptyDirVolumeSource{},
+					//					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
path: internal/controller/ols_app_postgres_assets_test.go
user: login: syedriko
body: yep, this is WIP
created_at: 2025-05-12T16:41:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,6 +110,10 @@ type OLSConfigReconcilerOptions struct {
 // NetworkPolicy for restricting access to OLS pods
 // +kubebuilder:rbac:groups=networking.k8s.io,namespace=openshift-lightspeed,resources=networkpolicies,verbs=get;list;watch;create;update;patch;delete
 
+// PVC access for the Postgres PVC
+// +kubebuilder:rbac:groups=core,namespace=openshift-lightspeed,resources=persistentvolumeclaims,verbs=get;list;watch;create;update;patch;delete
path: internal/controller/olsconfig_controller.go
user: login: xrajesh
body: @syedriko operator can be installed in any namespace , we will not have openshift-lightspeed namespace at that time.
created_at: 2025-05-14T03:34:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,6 +110,10 @@ type OLSConfigReconcilerOptions struct {
 // NetworkPolicy for restricting access to OLS pods
 // +kubebuilder:rbac:groups=networking.k8s.io,namespace=openshift-lightspeed,resources=networkpolicies,verbs=get;list;watch;create;update;patch;delete
 
+// PVC access for the Postgres PVC
+// +kubebuilder:rbac:groups=core,namespace=openshift-lightspeed,resources=persistentvolumeclaims,verbs=get;list;watch;create;update;patch;delete
path: internal/controller/olsconfig_controller.go
user: login: syedriko
body: @xrajesh PTAL. I removed `namespace=openshift-lightspeed` everywhere in that file and replaced `groups=core` with `groups=""` as it's more idiomatic.
created_at: 2025-05-14T03:49:45Z
author_association: CONTRIBUTOR

PR comments:
user: login: openshift-ci-robot
created_at: 2025-05-05T03:07:34Z
body: @syedriko: This pull request references [OLS-1686](https://issues.redhat.com//browse/OLS-1686) which is a valid jira issue.

<details>

In response to [this](https://github.com/openshift/lightspeed-operator/pull/728):

>## Description
>
>Added a PVC for the Postgres data volume
>
>## Type of change
>
>- [ ] Refactor
>- [x] New feature
>- [ ] Bug fix
>- [ ] CVE fix
>- [ ] Optimization
>- [ ] Documentation Update
>- [ ] Configuration Update
>- [ ] Bump-up dependent library
>
>## Related Tickets & Documents
>
>- Related Issue #
>- Closes # [OLS-1686](https://issues.redhat.com//browse/OLS-1686)
>
>## Checklist before requesting a review
>
>- [x] I have performed a self-review of my code.
>- [ ] PR has passed all pre-merge test jobs.
>- [ ] If it is a core feature, I have added thorough tests.
>
>## Testing
>- Please provide detailed steps to perform tests related to this code change.
>- How were the fix/results from this change verified? Please provide relevant screenshots or results.
>


Instructions for interacting with me using PR comments are available [here](https://prow.ci.openshift.org/command-help?repo=openshift%2Flightspeed-operator).  If you have questions or suggestions related to my behavior, please file an issue against the [openshift-eng/jira-lifecycle-plugin](https://github.com/openshift-eng/jira-lifecycle-plugin/issues/new) repository.
</details>
author_association: NONE
user: login: raptorsun
created_at: 2025-05-06T18:48:15Z
body: shall we disable the persistent volume claim during the E2E test? test clusters do not seem to have persistent volumes.
I think we should have an option in the OLSConfig CR for the user to opt out the persistency. 
author_association: CONTRIBUTOR
user: login: xrajesh
created_at: 2025-05-06T18:51:32Z
body: > shall we disable the persistent volume claim during the E2E test? test clusters do not seem to have persistent volumes. I think we should have an option in the OLSConfig CR for the user to opt out the persistency.

Storage setting is optional right - user need not do anything to opt out ? @syedriko ?
author_association: CONTRIBUTOR
user: login: syedriko
created_at: 2025-05-06T22:27:05Z
body: > > shall we disable the persistent volume claim during the E2E test? test clusters do not seem to have persistent volumes. I think we should have an option in the OLSConfig CR for the user to opt out the persistency.
> 
> Storage setting is optional right - user need not do anything to opt out ? @syedriko ?

As it's written now, persistence is mandatory, as I'd argue it should be. There're two knobs - one for PVC size and another one for storage class and they are optional with detaults, but persistence is always there. I was actually hoping to add automated e2e tests for persistence...
author_association: CONTRIBUTOR
user: login: raptorsun
created_at: 2025-05-07T16:27:05Z
body: > > > shall we disable the persistent volume claim during the E2E test? test clusters do not seem to have persistent volumes. I think we should have an option in the OLSConfig CR for the user to opt out the persistency.
> > 
> > 
> > Storage setting is optional right - user need not do anything to opt out ? @syedriko ?
> 
> As it's written now, persistence is mandatory, as I'd argue it should be. There're two knobs - one for PVC size and another one for storage class and they are optional with detaults, but persistence is always there. I was actually hoping to add automated e2e tests for persistence...

let's try create a PV in the emphemeral cluster. According to testplatform team, we can create PV in our test cluster in the emphemeral clusters.
I think we can use some PV like this 
```
apiVersion: v1
kind: PersistentVolume
metadata:
  name: lightspeed-postgres-pv
spec:
  storageClassName: ""
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  local:
    path: /tmp
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/os
              operator: In
              values:
                - linux
  claimRef:
    name: lightspeed-postgres-pvc
    namespace: openshift-lightspeed
```
Its `claimRef` correspond to the PVC creatred in this PR.
author_association: CONTRIBUTOR
user: login: syedriko
created_at: 2025-05-12T16:41:43Z
body: /hold
author_association: CONTRIBUTOR
user: login: syedriko
created_at: 2025-05-13T03:01:43Z
body: /unhold
author_association: CONTRIBUTOR
user: login: syedriko
created_at: 2025-05-13T03:05:35Z
body: @xrajesh @raptorsun PTAL. I described the persistent storage behavior in https://issues.redhat.com/browse/OLS-1754. By default, there's no persistence, it needs to be enabled explicitly.
author_association: CONTRIBUTOR
user: login: raptorsun
created_at: 2025-05-13T15:02:36Z
body: /lgtm
author_association: CONTRIBUTOR
user: login: syedriko
created_at: 2025-05-13T15:47:39Z
body: /retest
author_association: CONTRIBUTOR
user: login: raptorsun
created_at: 2025-05-14T13:11:14Z
body: /lgtm
author_association: CONTRIBUTOR
user: login: raptorsun
created_at: 2025-05-15T16:51:04Z
body: /approve
author_association: CONTRIBUTOR
user: login: raptorsun
created_at: 2025-05-15T17:58:31Z
body: /lgtm
author_association: CONTRIBUTOR
user: login: syedriko
created_at: 2025-05-15T19:08:43Z
body: /retest
author_association: CONTRIBUTOR
user: login: raptorsun
created_at: 2025-05-15T20:24:23Z
body: /override "Red Hat Konflux / service-e2e-tests-417 / ols-bundle"
author_association: CONTRIBUTOR
user: login: raptorsun
created_at: 2025-05-15T20:24:46Z
body: `Red Hat Konflux / service-e2e-tests-417 / ols-bundle` failed only tool calling cases
author_association: CONTRIBUTOR
user: login: openshift-ci[bot]
created_at: 2025-05-15T20:25:07Z
body: @raptorsun: Overrode contexts on behalf of raptorsun: Red Hat Konflux / service-e2e-tests-417 / ols-bundle

<details>

In response to [this](https://github.com/openshift/lightspeed-operator/pull/728#issuecomment-2884971621):

>/override "Red Hat Konflux / service-e2e-tests-417 / ols-bundle"


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository.
</details>
author_association: NONE
user: login: xrajesh
created_at: 2025-05-15T20:42:10Z
body: /approve
author_association: CONTRIBUTOR
user: login: openshift-ci[bot]
created_at: 2025-05-15T20:42:30Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/openshift/lightspeed-operator/pull/728#issuecomment-2884476544" title="Approved">raptorsun</a>*, *<a href="https://github.com/openshift/lightspeed-operator/pull/728#issuecomment-2885008116" title="Approved">xrajesh</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=openshift%2Flightspeed-operator).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/openshift/lightspeed-operator/blob/main/OWNERS)~~ [raptorsun,xrajesh]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: NONE
user: login: openshift-ci[bot]
created_at: 2025-05-15T22:01:09Z
body: @syedriko: all tests **passed!**


[Full PR test history](https://prow.ci.openshift.org/pr-history?org=openshift&repo=lightspeed-operator&pr=728). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:syedriko).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
author_association: NONE