From 04ee1bf5e3a996441cda72f0da2a5a5bb4c1c92d Mon Sep 17 00:00:00 2001 From: Jeremy Rickards Date: Tue, 17 Feb 2026 09:42:54 +0100 Subject: [PATCH 1/6] Update StorageConfiguration API Signed-off-by: Jeremy Rickards --- api/v1alpha2/perses_types.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/api/v1alpha2/perses_types.go b/api/v1alpha2/perses_types.go index 0247edc8..6e6c0b1d 100644 --- a/api/v1alpha2/perses_types.go +++ b/api/v1alpha2/perses_types.go @@ -18,7 +18,6 @@ package v1alpha2 import ( corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -88,7 +87,6 @@ type PersesSpec struct { TLS *TLS `json:"tls,omitempty"` // Storage configuration used by the StatefulSet // +operator-sdk:csv:customresourcedefinitions:type=spec - // +kubebuilder:default:={size: "1Gi"} // +optional Storage *StorageConfiguration `json:"storage,omitempty"` // ServiceAccountName is the name of the ServiceAccount to use for the Perses deployment or statefulset @@ -256,15 +254,18 @@ type Certificate struct { } // StorageConfiguration is the configuration used to create and reconcile PVCs +// +kubebuilder:validation:XValidation:rule="!(has(self.emptyDir) && has(self.pvcTemplate))",message="emptyDir and pvcTemplate are mutually exclusive" type StorageConfiguration struct { - // StorageClass specifies the StorageClass to use for PersistentVolumeClaims - // If not specified, the default StorageClass will be used + // EmptyDir to use for ephemeral storage. + // When set, data will be lost when the pod is deleted or restarted. + // Mutually exclusive with PersistentVolumeClaimTemplate. // +optional - StorageClass *string `json:"storageClass,omitempty"` - // Size specifies the storage capacity for the PersistentVolumeClaim - // Once set, the size cannot be decreased (only increased if the StorageClass supports volume expansion) + EmptyDir *corev1.EmptyDirVolumeSource `json:"emptyDir,omitempty"` + + // PersistentVolumeClaimTemplate is the template for PVCs that will be created. + // Mutually exclusive with EmptyDir. // +optional - Size *resource.Quantity `json:"size,omitempty"` + PersistentVolumeClaimTemplate *corev1.PersistentVolumeClaimSpec `json:"pvcTemplate,omitempty"` } // PersesStatus defines the observed state of Perses From 65991ac28d060f976623e81bff1b24c93f250643 Mon Sep 17 00:00:00 2001 From: Jeremy Rickards Date: Tue, 17 Feb 2026 09:47:11 +0100 Subject: [PATCH 2/6] Run `make manifests` and `make generate` Signed-off-by: Jeremy Rickards --- api/v1alpha1/zz_generated.conversion.go | 8 +- api/v1alpha2/zz_generated.deepcopy.go | 16 +- config/crd/bases/perses.dev_perses.yaml | 239 +++++++++++++++++- .../0persesCustomResourceDefinition.yaml | 235 ++++++++++++++++- jsonnet/generated/perses.dev_perses-crd.json | 205 +++++++++++++-- 5 files changed, 646 insertions(+), 57 deletions(-) diff --git a/api/v1alpha1/zz_generated.conversion.go b/api/v1alpha1/zz_generated.conversion.go index 5f2ae8d8..a00fb458 100644 --- a/api/v1alpha1/zz_generated.conversion.go +++ b/api/v1alpha1/zz_generated.conversion.go @@ -1098,14 +1098,14 @@ func Convert_v1alpha2_SecretSource_To_v1alpha1_SecretSource(in *v1alpha2.SecretS } func autoConvert_v1alpha1_StorageConfiguration_To_v1alpha2_StorageConfiguration(in *StorageConfiguration, out *v1alpha2.StorageConfiguration, s conversion.Scope) error { - out.StorageClass = in.StorageClass - // WARNING: in.Size requires manual conversion: inconvertible types (k8s.io/apimachinery/pkg/api/resource.Quantity vs *k8s.io/apimachinery/pkg/api/resource.Quantity) + // WARNING: in.StorageClass requires manual conversion: does not exist in peer-type + // WARNING: in.Size requires manual conversion: does not exist in peer-type return nil } func autoConvert_v1alpha2_StorageConfiguration_To_v1alpha1_StorageConfiguration(in *v1alpha2.StorageConfiguration, out *StorageConfiguration, s conversion.Scope) error { - out.StorageClass = in.StorageClass - // WARNING: in.Size requires manual conversion: inconvertible types (*k8s.io/apimachinery/pkg/api/resource.Quantity vs k8s.io/apimachinery/pkg/api/resource.Quantity) + // WARNING: in.EmptyDir requires manual conversion: does not exist in peer-type + // WARNING: in.PersistentVolumeClaimTemplate requires manual conversion: does not exist in peer-type return nil } diff --git a/api/v1alpha2/zz_generated.deepcopy.go b/api/v1alpha2/zz_generated.deepcopy.go index fb3e3ea2..3565663a 100644 --- a/api/v1alpha2/zz_generated.deepcopy.go +++ b/api/v1alpha2/zz_generated.deepcopy.go @@ -837,15 +837,15 @@ func (in *SecretVersion) DeepCopy() *SecretVersion { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StorageConfiguration) DeepCopyInto(out *StorageConfiguration) { *out = *in - if in.StorageClass != nil { - in, out := &in.StorageClass, &out.StorageClass - *out = new(string) - **out = **in + if in.EmptyDir != nil { + in, out := &in.EmptyDir, &out.EmptyDir + *out = new(v1.EmptyDirVolumeSource) + (*in).DeepCopyInto(*out) } - if in.Size != nil { - in, out := &in.Size, &out.Size - x := (*in).DeepCopy() - *out = &x + if in.PersistentVolumeClaimTemplate != nil { + in, out := &in.PersistentVolumeClaimTemplate, &out.PersistentVolumeClaimTemplate + *out = new(v1.PersistentVolumeClaimSpec) + (*in).DeepCopyInto(*out) } } diff --git a/config/crd/bases/perses.dev_perses.yaml b/config/crd/bases/perses.dev_perses.yaml index 0d5f5508..a31e89f7 100644 --- a/config/crd/bases/perses.dev_perses.yaml +++ b/config/crd/bases/perses.dev_perses.yaml @@ -5694,25 +5694,236 @@ spec: to use for the Perses deployment or statefulset type: string storage: - default: - size: 1Gi description: Storage configuration used by the StatefulSet properties: - size: - anyOf: - - type: integer - - type: string + emptyDir: description: |- - Size specifies the storage capacity for the PersistentVolumeClaim - Once set, the size cannot be decreased (only increased if the StorageClass supports volume expansion) - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - storageClass: + EmptyDir to use for ephemeral storage. + When set, data will be lost when the pod is deleted or restarted. + Mutually exclusive with PersistentVolumeClaimTemplate. + properties: + medium: + description: |- + medium represents what type of storage medium should back this directory. + The default is "" which means to use the node's default medium. + Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: |- + sizeLimit is the total amount of local storage required for this EmptyDir volume. + The size limit is also applicable for memory medium. + The maximum usage on memory medium EmptyDir would be the minimum value between + the SizeLimit specified here and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + pvcTemplate: description: |- - StorageClass specifies the StorageClass to use for PersistentVolumeClaims - If not specified, the default StorageClass will be used - type: string + PersistentVolumeClaimTemplate is the template for PVCs that will be created. + Mutually exclusive with EmptyDir. + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + Users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over volumes to consider + for binding. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeAttributesClassName: + description: |- + volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. + If specified, the CSI driver will create or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, + it can be changed after the claim is created. An empty string or nil value indicates that no + VolumeAttributesClass will be applied to the claim. If the claim enters an Infeasible error state, + this field can be reset to its previous value (including nil) to cancel the modification. + If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be + set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource + exists. + More info: https://kubernetes.io/docs/concepts/storage/volume-attributes-classes/ + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference to the PersistentVolume + backing this claim. + type: string + type: object type: object + x-kubernetes-validations: + - message: emptyDir and pvcTemplate are mutually exclusive + rule: '!(has(self.emptyDir) && has(self.pvcTemplate))' tls: description: TLS specifies the TLS configuration for the Perses instance properties: diff --git a/jsonnet/examples/0persesCustomResourceDefinition.yaml b/jsonnet/examples/0persesCustomResourceDefinition.yaml index d6bb6a49..bd0425cb 100644 --- a/jsonnet/examples/0persesCustomResourceDefinition.yaml +++ b/jsonnet/examples/0persesCustomResourceDefinition.yaml @@ -5367,25 +5367,232 @@ spec: description: ServiceAccountName is the name of the ServiceAccount to use for the Perses deployment or statefulset type: string storage: - default: - size: 1Gi description: Storage configuration used by the StatefulSet properties: - size: - anyOf: - - type: integer - - type: string + emptyDir: description: |- - Size specifies the storage capacity for the PersistentVolumeClaim - Once set, the size cannot be decreased (only increased if the StorageClass supports volume expansion) - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - storageClass: + EmptyDir to use for ephemeral storage. + When set, data will be lost when the pod is deleted or restarted. + Mutually exclusive with PersistentVolumeClaimTemplate. + properties: + medium: + description: |- + medium represents what type of storage medium should back this directory. + The default is "" which means to use the node's default medium. + Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: |- + sizeLimit is the total amount of local storage required for this EmptyDir volume. + The size limit is also applicable for memory medium. + The maximum usage on memory medium EmptyDir would be the minimum value between + the SizeLimit specified here and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + pvcTemplate: description: |- - StorageClass specifies the StorageClass to use for PersistentVolumeClaims - If not specified, the default StorageClass will be used - type: string + PersistentVolumeClaimTemplate is the template for PVCs that will be created. + Mutually exclusive with EmptyDir. + properties: + accessModes: + description: |- + accessModes contains the desired access modes the volume should have. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + items: + type: string + type: array + x-kubernetes-list-type: atomic + dataSource: + description: |- + dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller can support the specified data source, + it will create a new volume based on the contents of the specified data source. + When the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef, + and dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will not be copied to dataSource. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + dataSourceRef: + description: |- + dataSourceRef specifies the object from which to populate the volume with data, if a non-empty + volume is desired. This may be any object from a non-empty API group (non + core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only succeed if the type of + the specified object matches some installed volume populator or dynamic + provisioner. + This field will replace the functionality of the dataSource field and as such + if both fields are non-empty, they must have the same value. For backwards + compatibility, when namespace isn't specified in dataSourceRef, + both fields (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other is non-empty. + When namespace is specified in dataSourceRef, + dataSource isn't set to the same value and must be empty. + There are three important differences between dataSource and dataSourceRef: + * While dataSource only allows two specific types of objects, dataSourceRef + allows any non-core object, as well as PersistentVolumeClaim objects. + * While dataSource ignores disallowed values (dropping them), dataSourceRef + preserves all values, and generates an error if a disallowed value is + specified. + * While dataSource only allows local objects, dataSourceRef allows objects + in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled. + (Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + properties: + apiGroup: + description: |- + APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in the core API group. + For any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: |- + Namespace is the namespace of resource being referenced + Note that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details. + (Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: |- + resources represents the minimum resources the volume should have. + Users are allowed to specify resource requirements + that are lower than previous value but must still be higher than capacity recorded in the + status field of the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + selector: + description: selector is a label query over volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + storageClassName: + description: |- + storageClassName is the name of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + type: string + volumeAttributesClassName: + description: |- + volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim. + If specified, the CSI driver will create or update the volume with the attributes defined + in the corresponding VolumeAttributesClass. This has a different purpose than storageClassName, + it can be changed after the claim is created. An empty string or nil value indicates that no + VolumeAttributesClass will be applied to the claim. If the claim enters an Infeasible error state, + this field can be reset to its previous value (including nil) to cancel the modification. + If the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be + set to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource + exists. + More info: https://kubernetes.io/docs/concepts/storage/volume-attributes-classes/ + type: string + volumeMode: + description: |- + volumeMode defines what type of volume is required by the claim. + Value of Filesystem is implied when not included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference to the PersistentVolume backing this claim. + type: string + type: object type: object + x-kubernetes-validations: + - message: emptyDir and pvcTemplate are mutually exclusive + rule: '!(has(self.emptyDir) && has(self.pvcTemplate))' tls: description: TLS specifies the TLS configuration for the Perses instance properties: diff --git a/jsonnet/generated/perses.dev_perses-crd.json b/jsonnet/generated/perses.dev_perses-crd.json index 52ed9df7..75c29292 100644 --- a/jsonnet/generated/perses.dev_perses-crd.json +++ b/jsonnet/generated/perses.dev_perses-crd.json @@ -5718,30 +5718,201 @@ "type": "string" }, "storage": { - "default": { - "size": "1Gi" - }, "description": "Storage configuration used by the StatefulSet", "properties": { - "size": { - "anyOf": [ - { - "type": "integer" - }, - { + "emptyDir": { + "description": "EmptyDir to use for ephemeral storage.\nWhen set, data will be lost when the pod is deleted or restarted.\nMutually exclusive with PersistentVolumeClaimTemplate.", + "properties": { + "medium": { + "description": "medium represents what type of storage medium should back this directory.\nThe default is \"\" which means to use the node's default medium.\nMust be an empty string (default) or Memory.\nMore info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir", "type": "string" + }, + "sizeLimit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "description": "sizeLimit is the total amount of local storage required for this EmptyDir volume.\nThe size limit is also applicable for memory medium.\nThe maximum usage on memory medium EmptyDir would be the minimum value between\nthe SizeLimit specified here and the sum of memory limits of all containers in a pod.\nThe default is nil which means that the limit is undefined.\nMore info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir", + "pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$", + "x-kubernetes-int-or-string": true } - ], - "description": "Size specifies the storage capacity for the PersistentVolumeClaim\nOnce set, the size cannot be decreased (only increased if the StorageClass supports volume expansion)", - "pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$", - "x-kubernetes-int-or-string": true + }, + "type": "object" }, - "storageClass": { - "description": "StorageClass specifies the StorageClass to use for PersistentVolumeClaims\nIf not specified, the default StorageClass will be used", - "type": "string" + "pvcTemplate": { + "description": "PersistentVolumeClaimTemplate is the template for PVCs that will be created.\nMutually exclusive with EmptyDir.", + "properties": { + "accessModes": { + "description": "accessModes contains the desired access modes the volume should have.\nMore info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1", + "items": { + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "dataSource": { + "description": "dataSource field can be used to specify either:\n* An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot)\n* An existing PVC (PersistentVolumeClaim)\nIf the provisioner or an external controller can support the specified data source,\nit will create a new volume based on the contents of the specified data source.\nWhen the AnyVolumeDataSource feature gate is enabled, dataSource contents will be copied to dataSourceRef,\nand dataSourceRef contents will be copied to dataSource when dataSourceRef.namespace is not specified.\nIf the namespace is specified, then dataSourceRef will not be copied to dataSource.", + "properties": { + "apiGroup": { + "description": "APIGroup is the group for the resource being referenced.\nIf APIGroup is not specified, the specified Kind must be in the core API group.\nFor any other third-party types, APIGroup is required.", + "type": "string" + }, + "kind": { + "description": "Kind is the type of resource being referenced", + "type": "string" + }, + "name": { + "description": "Name is the name of resource being referenced", + "type": "string" + } + }, + "required": [ + "kind", + "name" + ], + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "dataSourceRef": { + "description": "dataSourceRef specifies the object from which to populate the volume with data, if a non-empty\nvolume is desired. This may be any object from a non-empty API group (non\ncore object) or a PersistentVolumeClaim object.\nWhen this field is specified, volume binding will only succeed if the type of\nthe specified object matches some installed volume populator or dynamic\nprovisioner.\nThis field will replace the functionality of the dataSource field and as such\nif both fields are non-empty, they must have the same value. For backwards\ncompatibility, when namespace isn't specified in dataSourceRef,\nboth fields (dataSource and dataSourceRef) will be set to the same\nvalue automatically if one of them is empty and the other is non-empty.\nWhen namespace is specified in dataSourceRef,\ndataSource isn't set to the same value and must be empty.\nThere are three important differences between dataSource and dataSourceRef:\n* While dataSource only allows two specific types of objects, dataSourceRef\n allows any non-core object, as well as PersistentVolumeClaim objects.\n* While dataSource ignores disallowed values (dropping them), dataSourceRef\n preserves all values, and generates an error if a disallowed value is\n specified.\n* While dataSource only allows local objects, dataSourceRef allows objects\n in any namespaces.\n(Beta) Using this field requires the AnyVolumeDataSource feature gate to be enabled.\n(Alpha) Using the namespace field of dataSourceRef requires the CrossNamespaceVolumeDataSource feature gate to be enabled.", + "properties": { + "apiGroup": { + "description": "APIGroup is the group for the resource being referenced.\nIf APIGroup is not specified, the specified Kind must be in the core API group.\nFor any other third-party types, APIGroup is required.", + "type": "string" + }, + "kind": { + "description": "Kind is the type of resource being referenced", + "type": "string" + }, + "name": { + "description": "Name is the name of resource being referenced", + "type": "string" + }, + "namespace": { + "description": "Namespace is the namespace of resource being referenced\nNote that when a namespace is specified, a gateway.networking.k8s.io/ReferenceGrant object is required in the referent namespace to allow that namespace's owner to accept the reference. See the ReferenceGrant documentation for details.\n(Alpha) This field requires the CrossNamespaceVolumeDataSource feature gate to be enabled.", + "type": "string" + } + }, + "required": [ + "kind", + "name" + ], + "type": "object" + }, + "resources": { + "description": "resources represents the minimum resources the volume should have.\nUsers are allowed to specify resource requirements\nthat are lower than previous value but must still be higher than capacity recorded in the\nstatus field of the claim.\nMore info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources", + "properties": { + "limits": { + "additionalProperties": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$", + "x-kubernetes-int-or-string": true + }, + "description": "Limits describes the maximum amount of compute resources allowed.\nMore info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": "object" + }, + "requests": { + "additionalProperties": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$", + "x-kubernetes-int-or-string": true + }, + "description": "Requests describes the minimum amount of compute resources required.\nIf Requests is omitted for a container, it defaults to Limits if that is explicitly specified,\notherwise to an implementation-defined value. Requests cannot exceed Limits.\nMore info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/", + "type": "object" + } + }, + "type": "object" + }, + "selector": { + "description": "selector is a label query over volumes to consider for binding.", + "properties": { + "matchExpressions": { + "description": "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + "items": { + "description": "A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values.", + "properties": { + "key": { + "description": "key is the label key that the selector applies to.", + "type": "string" + }, + "operator": { + "description": "operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist.", + "type": "string" + }, + "values": { + "description": "values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch.", + "items": { + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + } + }, + "required": [ + "key", + "operator" + ], + "type": "object" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "matchLabels": { + "additionalProperties": { + "type": "string" + }, + "description": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is \"key\", the\noperator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + "type": "object" + } + }, + "type": "object", + "x-kubernetes-map-type": "atomic" + }, + "storageClassName": { + "description": "storageClassName is the name of the StorageClass required by the claim.\nMore info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1", + "type": "string" + }, + "volumeAttributesClassName": { + "description": "volumeAttributesClassName may be used to set the VolumeAttributesClass used by this claim.\nIf specified, the CSI driver will create or update the volume with the attributes defined\nin the corresponding VolumeAttributesClass. This has a different purpose than storageClassName,\nit can be changed after the claim is created. An empty string or nil value indicates that no\nVolumeAttributesClass will be applied to the claim. If the claim enters an Infeasible error state,\nthis field can be reset to its previous value (including nil) to cancel the modification.\nIf the resource referred to by volumeAttributesClass does not exist, this PersistentVolumeClaim will be\nset to a Pending state, as reflected by the modifyVolumeStatus field, until such as a resource\nexists.\nMore info: https://kubernetes.io/docs/concepts/storage/volume-attributes-classes/", + "type": "string" + }, + "volumeMode": { + "description": "volumeMode defines what type of volume is required by the claim.\nValue of Filesystem is implied when not included in claim spec.", + "type": "string" + }, + "volumeName": { + "description": "volumeName is the binding reference to the PersistentVolume backing this claim.", + "type": "string" + } + }, + "type": "object" } }, - "type": "object" + "type": "object", + "x-kubernetes-validations": [ + { + "message": "emptyDir and pvcTemplate are mutually exclusive", + "rule": "!(has(self.emptyDir) && has(self.pvcTemplate))" + } + ] }, "tls": { "description": "TLS specifies the TLS configuration for the Perses instance", From e52e70830755df9caf5a509aa154ea324b952613 Mon Sep 17 00:00:00 2001 From: Jeremy Rickards Date: Tue, 17 Feb 2026 09:58:13 +0100 Subject: [PATCH 3/6] Update controller logic Signed-off-by: Jeremy Rickards --- controllers/perses/deployment_controller.go | 10 +++-- controllers/perses/statefulset_controller.go | 24 +++++++----- controllers/perses_controller_test.go | 8 +++- internal/perses/common/volumes.go | 41 ++++++++++++-------- 4 files changed, 53 insertions(+), 30 deletions(-) diff --git a/controllers/perses/deployment_controller.go b/controllers/perses/deployment_controller.go index adefffa3..8442acaa 100644 --- a/controllers/perses/deployment_controller.go +++ b/controllers/perses/deployment_controller.go @@ -45,14 +45,18 @@ func (r *PersesReconciler) reconcileDeployment(ctx context.Context, req ctrl.Req dlog.Error("perses not found in context") return subreconciler.RequeueWithError(fmt.Errorf("perses not found in context")) } + // Create Deployment if using SQL database OR file database with EmptyDir storage + usesSQLDatabase := perses.Spec.Config.Database.SQL != nil + usesFileWithEmptyDir := perses.Spec.Config.Database.File != nil && + perses.Spec.Storage != nil && perses.Spec.Storage.EmptyDir != nil - if perses.Spec.Config.Database.SQL == nil { - dlog.Debug("Database SQL configuration is not set, skipping Deployment creation") + if !usesSQLDatabase && !usesFileWithEmptyDir { + dlog.Debug("Neither SQL database nor file database with EmptyDir configured, skipping Deployment creation") found := &appsv1.Deployment{} err := r.Get(ctx, types.NamespacedName{Name: perses.Name, Namespace: perses.Namespace}, found) if err == nil { - dlog.Info("Deleting Deployment since database configuration changed") + dlog.Info("Deleting Deployment since configuration changed") if err := r.Delete(ctx, found); err != nil { dlog.WithError(err).Error("Failed to delete Deployment") return subreconciler.RequeueWithError(err) diff --git a/controllers/perses/statefulset_controller.go b/controllers/perses/statefulset_controller.go index b04c2622..70a75b27 100644 --- a/controllers/perses/statefulset_controller.go +++ b/controllers/perses/statefulset_controller.go @@ -47,13 +47,15 @@ func (r *PersesReconciler) reconcileStatefulSet(ctx context.Context, req ctrl.Re return subreconciler.RequeueWithError(fmt.Errorf("perses not found in context")) } - if perses.Spec.Config.Database.File == nil { - stlog.Debug("Database file configuration is not set, skipping StatefulSet creation") + // Create StatefulSet only if using file database AND NOT using EmptyDir (i.e., using PVC) + if perses.Spec.Config.Database.File == nil || + (perses.Spec.Storage != nil && perses.Spec.Storage.EmptyDir != nil) { + stlog.Debug("File database not configured or EmptyDir storage configured, skipping StatefulSet creation") found := &appsv1.StatefulSet{} err := r.Get(ctx, types.NamespacedName{Name: perses.Name, Namespace: perses.Namespace}, found) if err == nil { - stlog.Info("Deleting StatefulSet since database configuration changed") + stlog.Info("Deleting StatefulSet since configuration changed") if err := r.Delete(ctx, found); err != nil { stlog.WithError(err).Error("Failed to delete StatefulSet") return subreconciler.RequeueWithError(err) @@ -221,14 +223,16 @@ func (r *PersesReconciler) createPersesStatefulSet( sts.Spec.Template.Spec.Containers[0].Resources = *perses.Spec.Resources } - if perses.Spec.Storage != nil { - if perses.Spec.Storage.StorageClass != nil && len(*perses.Spec.Storage.StorageClass) > 0 { - sts.Spec.VolumeClaimTemplates[0].Spec.StorageClassName = perses.Spec.Storage.StorageClass - } + if perses.Spec.Storage != nil && perses.Spec.Storage.PersistentVolumeClaimTemplate != nil { + pvcTemplate := perses.Spec.Storage.PersistentVolumeClaimTemplate + + // Apply user-provided PVC template spec + sts.Spec.VolumeClaimTemplates[0].Spec = *pvcTemplate - if perses.Spec.Storage.Size != nil && !perses.Spec.Storage.Size.IsZero() { - sts.Spec.VolumeClaimTemplates[0].Spec.Resources.Requests = corev1.ResourceList{ - corev1.ResourceStorage: *perses.Spec.Storage.Size, + // Set default AccessModes if not specified + if pvcTemplate.AccessModes == nil { + sts.Spec.VolumeClaimTemplates[0].Spec.AccessModes = []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, } } } diff --git a/controllers/perses_controller_test.go b/controllers/perses_controller_test.go index d064a7c0..ddeedd21 100644 --- a/controllers/perses_controller_test.go +++ b/controllers/perses_controller_test.go @@ -311,7 +311,13 @@ var _ = Describe("Perses controller", func() { }, }, Storage: &persesv1alpha2.StorageConfiguration{ - Size: ptr.To(resource.MustParse("10Gi")), + PersistentVolumeClaimTemplate: &corev1.PersistentVolumeClaimSpec{ + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("10Gi"), + }, + }, + }, }, }, } diff --git a/internal/perses/common/volumes.go b/internal/perses/common/volumes.go index a09a03d0..9d3d7cc4 100644 --- a/internal/perses/common/volumes.go +++ b/internal/perses/common/volumes.go @@ -32,22 +32,27 @@ func GetVolumes(perses *v1alpha2.Perses) []corev1.Volume { }, } + // Add storage volume only for file-based database + // SQL database doesn't need storage volumes (uses external database) if perses.Spec.Config.Database.File != nil { - volumes = append(volumes, corev1.Volume{ - Name: StorageVolumeName, - VolumeSource: corev1.VolumeSource{ - PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: GetStorageName(perses.Name), + if perses.Spec.Storage != nil && perses.Spec.Storage.EmptyDir != nil { + volumes = append(volumes, corev1.Volume{ + Name: StorageVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: perses.Spec.Storage.EmptyDir, }, - }, - }) - } else { - volumes = append(volumes, corev1.Volume{ - Name: StorageVolumeName, - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }, - }) + }) + } else { + // File database without explicit emptyDir = use PVC (handled by StatefulSet) + volumes = append(volumes, corev1.Volume{ + Name: StorageVolumeName, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: GetStorageName(perses.Name), + }, + }, + }) + } } // Add TLS volumes if enabled @@ -132,11 +137,15 @@ func GetVolumeMounts(perses *v1alpha2.Perses) []corev1.VolumeMount { ReadOnly: true, MountPath: configMountPath, }, - { + } + + // Add storage volume mount only for file-based database + if perses.Spec.Config.Database.File != nil { + volumeMounts = append(volumeMounts, corev1.VolumeMount{ Name: StorageVolumeName, ReadOnly: false, MountPath: storageMountPath, - }, + }) } // Add TLS volume mounts if enabled From ed72b9851d006cba9b1fb34aed8db55bd8c0848a Mon Sep 17 00:00:00 2001 From: Jeremy Rickards Date: Tue, 17 Feb 2026 10:16:43 +0100 Subject: [PATCH 4/6] Update conversion webhook Signed-off-by: Jeremy Rickards --- api/v1alpha1/perses_conversion.go | 41 +++++++++++------ api/v1alpha1/perses_conversion_test.go | 63 ++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 13 deletions(-) diff --git a/api/v1alpha1/perses_conversion.go b/api/v1alpha1/perses_conversion.go index d7eb5529..6acd5b96 100644 --- a/api/v1alpha1/perses_conversion.go +++ b/api/v1alpha1/perses_conversion.go @@ -17,6 +17,7 @@ limitations under the License. package v1alpha1 import ( + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/conversion" conv "sigs.k8s.io/controller-runtime/pkg/conversion" @@ -80,27 +81,41 @@ func Convert_v1alpha2_OAuth_To_v1alpha1_OAuth(in *v1alpha2.OAuth, out *OAuth, s return nil } -// Convert_v1alpha1_StorageConfiguration_To_v1alpha2_StorageConfiguration converts StorageConfiguration from v1alpha1 to v1alpha2. +// Convert_v1alpha1_StorageConfiguration_To_v1alpha2_StorageConfiguration converts v1alpha1 storage to v1alpha2. +// Migrates StorageClass and Size fields into PersistentVolumeClaimTemplate. func Convert_v1alpha1_StorageConfiguration_To_v1alpha2_StorageConfiguration(in *StorageConfiguration, out *v1alpha2.StorageConfiguration, s conversion.Scope) error { - if err := autoConvert_v1alpha1_StorageConfiguration_To_v1alpha2_StorageConfiguration(in, out, s); err != nil { - return err + // Only create PVC template if v1alpha1 has storage config + if in.StorageClass == nil && in.Size.IsZero() { + return nil + } + + out.PersistentVolumeClaimTemplate = &corev1.PersistentVolumeClaimSpec{ + StorageClassName: in.StorageClass, } - // Convert Size from resource.Quantity to *resource.Quantity + if !in.Size.IsZero() { - size := in.Size.DeepCopy() - out.Size = &size + out.PersistentVolumeClaimTemplate.Resources.Requests = corev1.ResourceList{ + corev1.ResourceStorage: in.Size, + } } + return nil } -// Convert_v1alpha2_StorageConfiguration_To_v1alpha1_StorageConfiguration converts StorageConfiguration from v1alpha2 to v1alpha1. +// Convert_v1alpha2_StorageConfiguration_To_v1alpha1_StorageConfiguration converts v1alpha2 storage to v1alpha1. +// Extracts StorageClass and Size from PersistentVolumeClaimTemplate. func Convert_v1alpha2_StorageConfiguration_To_v1alpha1_StorageConfiguration(in *v1alpha2.StorageConfiguration, out *StorageConfiguration, s conversion.Scope) error { - if err := autoConvert_v1alpha2_StorageConfiguration_To_v1alpha1_StorageConfiguration(in, out, s); err != nil { - return err - } - // Convert Size from *resource.Quantity to resource.Quantity - if in.Size != nil { - out.Size = *in.Size + // Extract from PVC template if it exists + if in.PersistentVolumeClaimTemplate != nil { + out.StorageClass = in.PersistentVolumeClaimTemplate.StorageClassName + + if storage, ok := in.PersistentVolumeClaimTemplate.Resources.Requests[corev1.ResourceStorage]; ok { + out.Size = storage + } } + + // EmptyDir cannot be converted to v1alpha1 (not supported) + // It will be silently dropped + return nil } diff --git a/api/v1alpha1/perses_conversion_test.go b/api/v1alpha1/perses_conversion_test.go index f490f834..b95a2e3c 100644 --- a/api/v1alpha1/perses_conversion_test.go +++ b/api/v1alpha1/perses_conversion_test.go @@ -18,6 +18,10 @@ import ( "strings" "testing" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/utils/ptr" + "github.com/perses/perses-operator/api/v1alpha2" ) @@ -72,3 +76,62 @@ func TestBasicAuthConversion(t *testing.T) { t.Errorf("v1alpha1 should not use 'passwordPath', got: %s", string(v1JSONReverse)) } } + +func TestStorageConfigurationConversion(t *testing.T) { + storageClass := "fast-ssd" + size := resource.MustParse("10Gi") + + // Test v1alpha1 -> v1alpha2 conversion + v1Storage := &StorageConfiguration{ + StorageClass: &storageClass, + Size: size, + } + + var v2Storage v1alpha2.StorageConfiguration + if err := Convert_v1alpha1_StorageConfiguration_To_v1alpha2_StorageConfiguration(v1Storage, &v2Storage, nil); err != nil { + t.Fatalf("conversion v1->v2: %v", err) + } + + // Verify PVC template was created with correct values + if v2Storage.PersistentVolumeClaimTemplate == nil { + t.Fatal("expected PersistentVolumeClaimTemplate to be set") + } + if *v2Storage.PersistentVolumeClaimTemplate.StorageClassName != storageClass { + t.Errorf("expected storageClassName=%s, got %s", storageClass, *v2Storage.PersistentVolumeClaimTemplate.StorageClassName) + } + + storageReq := v2Storage.PersistentVolumeClaimTemplate.Resources.Requests[corev1.ResourceStorage] + if !storageReq.Equal(size) { + t.Errorf("expected size=%s, got %s", size.String(), storageReq.String()) + } + + // Test v1alpha2 -> v1alpha1 conversion (round trip) + var v1StorageReverse StorageConfiguration + if err := Convert_v1alpha2_StorageConfiguration_To_v1alpha1_StorageConfiguration(&v2Storage, &v1StorageReverse, nil); err != nil { + t.Fatalf("conversion v2->v1: %v", err) + } + + if v1StorageReverse.StorageClass == nil || *v1StorageReverse.StorageClass != storageClass { + t.Errorf("expected storageClass=%s after round trip", storageClass) + } + if !v1StorageReverse.Size.Equal(size) { + t.Errorf("expected size=%s after round trip, got %s", size.String(), v1StorageReverse.Size.String()) + } + + // Test v1alpha2 with EmptyDir (should drop EmptyDir when converting to v1alpha1) + v2StorageWithEmptyDir := v1alpha2.StorageConfiguration{ + EmptyDir: &corev1.EmptyDirVolumeSource{ + SizeLimit: ptr.To(resource.MustParse("1Gi")), + }, + } + + var v1StorageDropped StorageConfiguration + if err := Convert_v1alpha2_StorageConfiguration_To_v1alpha1_StorageConfiguration(&v2StorageWithEmptyDir, &v1StorageDropped, nil); err != nil { + t.Fatalf("conversion v2->v1 with emptyDir: %v", err) + } + + // v1alpha1 should have no storage config since EmptyDir isn't supported + if v1StorageDropped.StorageClass != nil || !v1StorageDropped.Size.IsZero() { + t.Error("expected empty storage config when converting emptyDir from v2 to v1") + } +} From 0e0a1c363db8e0633e32bf5070eec9fb8d0fb3f7 Mon Sep 17 00:00:00 2001 From: Jeremy Rickards Date: Tue, 17 Feb 2026 14:15:27 +0100 Subject: [PATCH 5/6] Address PR feedback Signed-off-by: Jeremy Rickards --- api/v1alpha2/perses_types.go | 16 ++++++++++++++++ controllers/perses/deployment_controller.go | 6 +----- controllers/perses/statefulset_controller.go | 4 +--- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/api/v1alpha2/perses_types.go b/api/v1alpha2/perses_types.go index 6e6c0b1d..7fb40d8c 100644 --- a/api/v1alpha2/perses_types.go +++ b/api/v1alpha2/perses_types.go @@ -296,6 +296,22 @@ type Perses struct { Status PersesStatus `json:"status,omitempty"` } +// RequiresDeployment returns true if the Perses instance should be deployed as a Deployment. +// This is the case when using SQL database OR file database with EmptyDir storage. +func (p *Perses) RequiresDeployment() bool { + usesSQLDatabase := p.Spec.Config.Database.SQL != nil + usesFileWithEmptyDir := p.Spec.Config.Database.File != nil && + p.Spec.Storage != nil && p.Spec.Storage.EmptyDir != nil + return usesSQLDatabase || usesFileWithEmptyDir +} + +// RequiresStatefulSet returns true if the Perses instance should be deployed as a StatefulSet. +// This is the case when using file database with persistent volume storage (not EmptyDir). +func (p *Perses) RequiresStatefulSet() bool { + return p.Spec.Config.Database.File != nil && + (p.Spec.Storage == nil || p.Spec.Storage.EmptyDir == nil) +} + //+kubebuilder:object:root=true // PersesList contains a list of Perses diff --git a/controllers/perses/deployment_controller.go b/controllers/perses/deployment_controller.go index 8442acaa..73cbbda6 100644 --- a/controllers/perses/deployment_controller.go +++ b/controllers/perses/deployment_controller.go @@ -45,12 +45,8 @@ func (r *PersesReconciler) reconcileDeployment(ctx context.Context, req ctrl.Req dlog.Error("perses not found in context") return subreconciler.RequeueWithError(fmt.Errorf("perses not found in context")) } - // Create Deployment if using SQL database OR file database with EmptyDir storage - usesSQLDatabase := perses.Spec.Config.Database.SQL != nil - usesFileWithEmptyDir := perses.Spec.Config.Database.File != nil && - perses.Spec.Storage != nil && perses.Spec.Storage.EmptyDir != nil - if !usesSQLDatabase && !usesFileWithEmptyDir { + if !perses.RequiresDeployment() { dlog.Debug("Neither SQL database nor file database with EmptyDir configured, skipping Deployment creation") found := &appsv1.Deployment{} diff --git a/controllers/perses/statefulset_controller.go b/controllers/perses/statefulset_controller.go index 70a75b27..cdbecf21 100644 --- a/controllers/perses/statefulset_controller.go +++ b/controllers/perses/statefulset_controller.go @@ -47,9 +47,7 @@ func (r *PersesReconciler) reconcileStatefulSet(ctx context.Context, req ctrl.Re return subreconciler.RequeueWithError(fmt.Errorf("perses not found in context")) } - // Create StatefulSet only if using file database AND NOT using EmptyDir (i.e., using PVC) - if perses.Spec.Config.Database.File == nil || - (perses.Spec.Storage != nil && perses.Spec.Storage.EmptyDir != nil) { + if !perses.RequiresStatefulSet() { stlog.Debug("File database not configured or EmptyDir storage configured, skipping StatefulSet creation") found := &appsv1.StatefulSet{} From 65af06192f13dcb870aac9fac59bc8309ff27db7 Mon Sep 17 00:00:00 2001 From: Jeremy Rickards Date: Tue, 17 Feb 2026 14:38:45 +0100 Subject: [PATCH 6/6] Run `make generate-api-docs` Signed-off-by: Jeremy Rickards --- docs/api.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api.md b/docs/api.md index da4dd95f..d31ca875 100644 --- a/docs/api.md +++ b/docs/api.md @@ -408,7 +408,7 @@ _Appears in:_ | `livenessProbe` _[Probe](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#probe-v1-core)_ | LivenessProbe specifies the liveness probe configuration for the Perses container | | Optional: \{\}
| | `readinessProbe` _[Probe](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#probe-v1-core)_ | ReadinessProbe specifies the readiness probe configuration for the Perses container | | Optional: \{\}
| | `tls` _[TLS](#tls)_ | TLS specifies the TLS configuration for the Perses instance | | Optional: \{\}
| -| `storage` _[StorageConfiguration](#storageconfiguration)_ | Storage configuration used by the StatefulSet | \{ size:1Gi \} | Optional: \{\}
| +| `storage` _[StorageConfiguration](#storageconfiguration)_ | Storage configuration used by the StatefulSet | | Optional: \{\}
| | `serviceAccountName` _string_ | ServiceAccountName is the name of the ServiceAccount to use for the Perses deployment or statefulset | | Optional: \{\}
| | `podSecurityContext` _[PodSecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#podsecuritycontext-v1-core)_ | PodSecurityContext holds pod-level security attributes and common container settings
If not specified, defaults to fsGroup: 65534 to ensure proper volume permissions for the nobody user | | Optional: \{\}
| | `logLevel` _string_ | LogLevel defines the log level for Perses | | Enum: [panic fatal error warning info debug trace]
Optional: \{\}
| @@ -538,8 +538,8 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `storageClass` _string_ | StorageClass specifies the StorageClass to use for PersistentVolumeClaims
If not specified, the default StorageClass will be used | | Optional: \{\}
| -| `size` _[Quantity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#quantity-resource-api)_ | Size specifies the storage capacity for the PersistentVolumeClaim
Once set, the size cannot be decreased (only increased if the StorageClass supports volume expansion) | | Optional: \{\}
| +| `emptyDir` _[EmptyDirVolumeSource](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#emptydirvolumesource-v1-core)_ | EmptyDir to use for ephemeral storage.
When set, data will be lost when the pod is deleted or restarted.
Mutually exclusive with PersistentVolumeClaimTemplate. | | Optional: \{\}
| +| `pvcTemplate` _[PersistentVolumeClaimSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#persistentvolumeclaimspec-v1-core)_ | PersistentVolumeClaimTemplate is the template for PVCs that will be created.
Mutually exclusive with EmptyDir. | | Optional: \{\}
| #### TLS