diff --git a/api/v1/qdrantcluster_types.go b/api/v1/qdrantcluster_types.go index 5efacee..523eec8 100644 --- a/api/v1/qdrantcluster_types.go +++ b/api/v1/qdrantcluster_types.go @@ -55,17 +55,6 @@ const ( ByCountAndSize RebalanceStrategy = "by_count_and_size" ) -// StorageTier specifies the performance profile for the disk to use. -// +kubebuilder:validation:Enum=budget;balanced;performance -type StorageTier string - -//goland:noinspection GoUnusedConst -const ( - StorageTierBudget StorageTier = "budget" - StorageTierBalanced StorageTier = "balanced" - StorageTierPerformance StorageTier = "performance" -) - // QdrantClusterSpec defines the desired state of QdrantCluster // +kubebuilder:pruning:PreserveUnknownFields type QdrantClusterSpec struct { @@ -128,10 +117,9 @@ type QdrantClusterSpec struct { // StorageClassNames specifies the storage class names for db and snapshots. // +optional StorageClassNames *StorageClassNames `json:"storageClassNames,omitempty"` - // StorageTier specifies the performance tier to use for the disk - // +kubebuilder:validation:Enum=budget;balanced;performance + // Storage specifies the storage specification for the PVCs of the cluster. If the field is not set, no configuration will be applied. // +optional - StorageTier *StorageTier `json:"storageTier,omitempty"` + Storage *Storage `json:"storage,omitempty"` // TopologySpreadConstraints specifies the topology spread constraints for the cluster. // +optional TopologySpreadConstraints *[]corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` @@ -169,6 +157,10 @@ func (s QdrantClusterSpec) Validate() error { if err := s.Resources.Validate("Spec.Resources"); err != nil { return err } + // Validate Storage configurations + if err := s.Storage.Validate(); err != nil { + return err + } return nil } @@ -790,6 +782,38 @@ func (n *StorageClassNames) GetSnapshots() *string { return n.Snapshots } +type Storage struct { + // VolumeAttributesClassName specifies VolumeAttributeClass name to use for the storage PVCs + // +optional + VolumeAttributesClassName *string `json:"volumeAttributesClassName,omitempty"` + // IOPS defines the IOPS number to configure for the storage PVCs + // +optional + IOPS *int `json:"iops,omitempty"` + // Throughput defines the throughput number in MB/s for the storage PVCs + // +optional + Throughput *int `json:"throughput,omitempty"` +} + +// Validate storage configurations +func (s *Storage) Validate() error { + if s == nil { + return nil + } + // User can specify either VolumeAttributesClassName or both IOPS and Throughput + if s.VolumeAttributesClassName != nil { + // Both IOPS and Throughput must be nil + if s.Throughput != nil || s.IOPS != nil { + return fmt.Errorf(".spec.storage: can not specify both VolumeAttributesClassName and IOPS/Throughput") + } + return nil + } + // Must specify either both IOPS and Throughput or none + if (s.IOPS == nil && s.Throughput == nil) || (s.IOPS != nil && s.Throughput != nil) { + return nil + } + return fmt.Errorf(".spec.storage: must specify both IOPS and Throughput") +} + type ClusterPhase string //goland:noinspection GoUnusedConst diff --git a/api/v1/qdrantcluster_types_test.go b/api/v1/qdrantcluster_types_test.go index 961a46b..a2564d7 100644 --- a/api/v1/qdrantcluster_types_test.go +++ b/api/v1/qdrantcluster_types_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "k8s.io/utils/ptr" ) func TestValidate(t *testing.T) { @@ -77,6 +78,103 @@ func TestValidate(t *testing.T) { }, expectedError: fmt.Errorf("Spec.Resources.Memory error: quantities must match the regular expression '^([+-]?[0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$'"), }, + { + name: "No storage configuration", + spec: QdrantClusterSpec{ + Resources: Resources{ + CPU: "100m", + Memory: "1Gi", + Storage: "2Gi", + }, + }, + expectedError: nil, + }, + { + name: "Empty storage configuration", + spec: QdrantClusterSpec{ + Resources: Resources{ + CPU: "100m", + Memory: "1Gi", + Storage: "2Gi", + }, + Storage: &Storage{}, + }, + expectedError: nil, + }, + { + name: "Only VolumeAttributeClassName specified", + spec: QdrantClusterSpec{ + Resources: Resources{ + CPU: "100m", + Memory: "1Gi", + Storage: "2Gi", + }, + Storage: &Storage{ + VolumeAttributesClassName: ptr.To("foo"), + }, + }, + expectedError: nil, + }, + + { + name: "Both VolumeAttributeClassName and IOPS/Throughput specified", + spec: QdrantClusterSpec{ + Resources: Resources{ + CPU: "100m", + Memory: "1Gi", + Storage: "2Gi", + }, + Storage: &Storage{ + VolumeAttributesClassName: ptr.To("foo"), + IOPS: ptr.To(10000), + Throughput: ptr.To(500), + }, + }, + expectedError: fmt.Errorf(".spec.storage: can not specify both VolumeAttributesClassName and IOPS/Throughput"), + }, + { + name: "Only IOPS specified", + spec: QdrantClusterSpec{ + Resources: Resources{ + CPU: "100m", + Memory: "1Gi", + Storage: "2Gi", + }, + Storage: &Storage{ + IOPS: ptr.To(10000), + }, + }, + expectedError: fmt.Errorf(".spec.storage: must specify both IOPS and Throughput"), + }, + { + name: "Only Throughput specified", + spec: QdrantClusterSpec{ + Resources: Resources{ + CPU: "100m", + Memory: "1Gi", + Storage: "2Gi", + }, + Storage: &Storage{ + Throughput: ptr.To(500), + }, + }, + expectedError: fmt.Errorf(".spec.storage: must specify both IOPS and Throughput"), + }, + { + name: "Both IOPS/Throughput specified", + spec: QdrantClusterSpec{ + Resources: Resources{ + CPU: "100m", + Memory: "1Gi", + Storage: "2Gi", + }, + Storage: &Storage{ + IOPS: ptr.To(10000), + Throughput: ptr.To(500), + }, + }, + expectedError: nil, + }, } for _, tt := range testCases { diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 978de84..44ea736 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -951,10 +951,10 @@ func (in *QdrantClusterSpec) DeepCopyInto(out *QdrantClusterSpec) { *out = new(StorageClassNames) (*in).DeepCopyInto(*out) } - if in.StorageTier != nil { - in, out := &in.StorageTier, &out.StorageTier - *out = new(StorageTier) - **out = **in + if in.Storage != nil { + in, out := &in.Storage, &out.Storage + *out = new(Storage) + (*in).DeepCopyInto(*out) } if in.TopologySpreadConstraints != nil { in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints @@ -1613,6 +1613,36 @@ func (in *RestoreSource) DeepCopy() *RestoreSource { 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 + if in.VolumeAttributesClassName != nil { + in, out := &in.VolumeAttributesClassName, &out.VolumeAttributesClassName + *out = new(string) + **out = **in + } + if in.IOPS != nil { + in, out := &in.IOPS, &out.IOPS + *out = new(int) + **out = **in + } + if in.Throughput != nil { + in, out := &in.Throughput, &out.Throughput + *out = new(int) + **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 *StorageClass) DeepCopyInto(out *StorageClass) { *out = *in diff --git a/charts/qdrant-kubernetes-api/templates/region-crds/qdrant.io_qdrantclusters.yaml b/charts/qdrant-kubernetes-api/templates/region-crds/qdrant.io_qdrantclusters.yaml index f2060f6..dc1449b 100644 --- a/charts/qdrant-kubernetes-api/templates/region-crds/qdrant.io_qdrantclusters.yaml +++ b/charts/qdrant-kubernetes-api/templates/region-crds/qdrant.io_qdrantclusters.yaml @@ -858,6 +858,24 @@ spec: type: object type: object type: object + storage: + description: Storage specifies the storage specification for the PVCs + of the cluster. If the field is not set, no configuration will be + applied. + properties: + iops: + description: IOPS defines the IOPS number to configure for the + storage PVCs + type: integer + throughput: + description: Throughput defines the throughput number in MB/s + for the storage PVCs + type: integer + volumeAttributesClassName: + description: VolumeAttributesClassName specifies VolumeAttributeClass + name to use for the storage PVCs + type: string + type: object storageClassNames: description: StorageClassNames specifies the storage class names for db and snapshots. @@ -870,19 +888,6 @@ spec: volume. type: string type: object - storageTier: - allOf: - - enum: - - budget - - balanced - - performance - - enum: - - budget - - balanced - - performance - description: StorageTier specifies the performance tier to use for - the disk - type: string suspend: default: false description: |- diff --git a/crds/qdrant.io_qdrantclusters.yaml b/crds/qdrant.io_qdrantclusters.yaml index 908a03c..01ca838 100644 --- a/crds/qdrant.io_qdrantclusters.yaml +++ b/crds/qdrant.io_qdrantclusters.yaml @@ -857,6 +857,24 @@ spec: type: object type: object type: object + storage: + description: Storage specifies the storage specification for the PVCs + of the cluster. If the field is not set, no configuration will be + applied. + properties: + iops: + description: IOPS defines the IOPS number to configure for the + storage PVCs + type: integer + throughput: + description: Throughput defines the throughput number in MB/s + for the storage PVCs + type: integer + volumeAttributesClassName: + description: VolumeAttributesClassName specifies VolumeAttributeClass + name to use for the storage PVCs + type: string + type: object storageClassNames: description: StorageClassNames specifies the storage class names for db and snapshots. @@ -869,19 +887,6 @@ spec: volume. type: string type: object - storageTier: - allOf: - - enum: - - budget - - balanced - - performance - - enum: - - budget - - balanced - - performance - description: StorageTier specifies the performance tier to use for - the disk - type: string suspend: default: false description: |- diff --git a/docs/api.md b/docs/api.md index 41cb91d..4258d65 100644 --- a/docs/api.md +++ b/docs/api.md @@ -796,7 +796,7 @@ _Appears in:_ | `gpu` _[GPU](#gpu)_ | GPU specifies GPU configuration for the cluster. If this field is not set, no GPU will be used. | | | | `statefulSet` _[KubernetesStatefulSet](#kubernetesstatefulset)_ | StatefulSet specifies the configuration of the Qdrant Kubernetes StatefulSet. | | | | `storageClassNames` _[StorageClassNames](#storageclassnames)_ | StorageClassNames specifies the storage class names for db and snapshots. | | | -| `storageTier` _[StorageTier](#storagetier)_ | StorageTier specifies the performance tier to use for the disk | | Enum: [budget balanced performance]
| +| `storage` _[Storage](#storage)_ | Storage specifies the storage specification for the PVCs of the cluster. If the field is not set, no configuration will be applied. | | | | `topologySpreadConstraints` _[TopologySpreadConstraint](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#topologyspreadconstraint-v1-core)_ | TopologySpreadConstraints specifies the topology spread constraints for the cluster. | | | | `podDisruptionBudget` _[PodDisruptionBudgetSpec](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#poddisruptionbudgetspec-v1-policy)_ | PodDisruptionBudget specifies the pod disruption budget for the cluster. | | | | `restartAllPodsConcurrently` _boolean_ | RestartAllPodsConcurrently specifies whether to restart all pods concurrently (also called one-shot-restart).
If enabled, all the pods in the cluster will be restarted concurrently in situations where multiple pods
need to be restarted, like when RestartedAtAnnotationKey is added/updated or the Qdrant version needs to be upgraded.
This helps sharded but not replicated clusters to reduce downtime to a possible minimum during restart.
If unset, the operator is going to restart nodes concurrently if none of the collections if replicated. | | | @@ -1290,6 +1290,24 @@ _Appears in:_ | `Disabled` | | +#### Storage + + + + + + + +_Appears in:_ +- [QdrantClusterSpec](#qdrantclusterspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `volumeAttributesClassName` _string_ | VolumeAttributesClassName specifies VolumeAttributeClass name to use for the storage PVCs | | | +| `iops` _integer_ | IOPS defines the IOPS number to configure for the storage PVCs | | | +| `throughput` _integer_ | Throughput defines the throughput number in MB/s for the storage PVCs | | | + + #### StorageClass @@ -1362,25 +1380,6 @@ _Appears in:_ | `async_scorer` _boolean_ | AsyncScorer enables io_uring when rescoring | | | -#### StorageTier - -_Underlying type:_ _string_ - -StorageTier specifies the performance profile for the disk to use. - -_Validation:_ -- Enum: [budget balanced performance] - -_Appears in:_ -- [QdrantClusterSpec](#qdrantclusterspec) - -| Field | Description | -| --- | --- | -| `budget` | | -| `balanced` | | -| `performance` | | - - #### TraefikConfig diff --git a/go.mod b/go.mod index 7d80c9b..a4b328e 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,6 @@ require ( k8s.io/apiextensions-apiserver v0.33.3 k8s.io/apimachinery v0.33.3 k8s.io/client-go v0.33.3 - k8s.io/utils v0.0.0-20241210054802-24370beab758 sigs.k8s.io/controller-runtime v0.21.0 ) @@ -68,6 +67,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect