Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EVEREST-1874 | support volume expansion for PXC #688

Merged
merged 25 commits into from
Mar 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions api/v1alpha1/databasecluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const (
AppStateRestoring AppState = "restoring"
// AppStateDeleting is a deleting state.
AppStateDeleting AppState = "deleting"
// AppStateResizingVolumes is the state when PVCs are being resized.
AppStateResizingVolumes = "resizingVolumes"
// AppStateNew represents a newly created cluster that has not yet been reconciled.
AppStateNew AppState = ""

Expand Down Expand Up @@ -355,6 +357,20 @@ type DatabaseClusterSpec struct {
Sharding *Sharding `json:"sharding,omitempty"`
}

const (
// ConditionTypeCannotResizeVolume is a condition type that indicates that the volume cannot be resized.
ConditionTypeCannotResizeVolume = "CannotResizeVolume"
)

const (
// ReasonStorageClassDoesNotSupportExpansion is a reason for condition ConditionTypeCannotExpandStorage
// when the storage class does not support volume expansion.
ReasonStorageClassDoesNotSupportExpansion = "StorageClassDoesNotSupportExpansion"
// ReasonCannotShrinkVolume is a reason for condition ConditionTypeCannotResizeVolume
// when the volume cannot be shrunk.
ReasonCannotShrinkVolume = "CannotShrinkVolume"
)

// DatabaseClusterStatus defines the observed state of DatabaseCluster.
type DatabaseClusterStatus struct {
// ObservedGeneration is the most recent generation observed for this DatabaseCluster.
Expand All @@ -381,6 +397,8 @@ type DatabaseClusterStatus struct {
RecommendedCRVersion *string `json:"recommendedCRVersion,omitempty"`
// Details provides full status of the upstream cluster as a plain text.
Details string `json:"details,omitempty"`
// Conditions contains the observed conditions of the DatabaseCluster.
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

//+kubebuilder:object:root=true
Expand Down
8 changes: 8 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ metadata:
}
]
capabilities: Basic Install
createdAt: "2025-02-14T15:05:36Z"
createdAt: "2025-03-12T11:23:36Z"
operators.operatorframework.io/builder: operator-sdk-v1.38.0
operators.operatorframework.io/project_layout: go.kubebuilder.io/v4
name: everest-operator.v0.0.0
Expand Down
57 changes: 57 additions & 0 deletions bundle/manifests/everest.percona.com_databaseclusters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,63 @@ spec:
activeStorage:
description: ActiveStorage is the storage used in cluster (psmdb only)
type: string
conditions:
description: Conditions contains the observed conditions of the DatabaseCluster.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
crVersion:
description: CRVersion is the observed version of the CR used with
the underlying operator.
Expand Down
57 changes: 57 additions & 0 deletions config/crd/bases/everest.percona.com_databaseclusters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,63 @@ spec:
activeStorage:
description: ActiveStorage is the storage used in cluster (psmdb only)
type: string
conditions:
description: Conditions contains the observed conditions of the DatabaseCluster.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
crVersion:
description: CRVersion is the observed version of the CR used with
the underlying operator.
Expand Down
56 changes: 56 additions & 0 deletions deploy/bundle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,62 @@ spec:
activeStorage:
description: ActiveStorage is the storage used in cluster (psmdb only)
type: string
conditions:
description: Conditions contains the observed conditions of the DatabaseCluster.
items:
description: Condition contains details for one aspect of the current state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
crVersion:
description: CRVersion is the observed version of the CR used with the underlying operator.
type: string
Expand Down
88 changes: 88 additions & 0 deletions internal/controller/common/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
storagev1 "k8s.io/api/storage/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/fields"
Expand Down Expand Up @@ -793,3 +794,90 @@ func toUnstructured(obj runtime.Object) (*unstructured.Unstructured, error) {
}
return ud, nil
}

const (
storageClassDefaultAnnotation = "storageclass.kubernetes.io/is-default-class"
)

func storageClassSupportsVolumeExpansion(ctx context.Context, c client.Client, className *string) (bool, error) {
storageClass, err := getStorageClassOrDefault(ctx, c, className)
if err != nil {
return false, fmt.Errorf("getStorageClassOrDefault failed: %w", err)
}
return *storageClass.AllowVolumeExpansion, nil
}

func getStorageClassOrDefault(ctx context.Context, c client.Client, scName *string) (*storagev1.StorageClass, error) {
storageClass := &storagev1.StorageClass{}
if scName == nil {
storageClasses := &storagev1.StorageClassList{}
if err := c.List(ctx, storageClasses); err != nil {
return nil, err
}
for _, sc := range storageClasses.Items {
if sc.Annotations[storageClassDefaultAnnotation] == "true" {
return &sc, nil
}
}
return nil, errors.New("no default storage class found")
}
if err := c.Get(ctx, types.NamespacedName{Name: *scName}, storageClass); err != nil {
return nil, err
}
return storageClass, nil
}

// ConfigureStorage handles storage configuration and volume expansion checks for the given database cluster.
func ConfigureStorage(
ctx context.Context,
c client.Client,
db *everestv1alpha1.DatabaseCluster,
currentSize resource.Quantity,
setStorageSizeFunc func(resource.Quantity),
) error {
meta.RemoveStatusCondition(&db.Status.Conditions, everestv1alpha1.ConditionTypeCannotResizeVolume)

desiredSize := db.Spec.Engine.Storage.Size
storageClass := db.Spec.Engine.Storage.Class

// We cannot shrink the volume size.
hasStorageShrunk := currentSize.Cmp(desiredSize) > 0 && !currentSize.IsZero()
if hasStorageShrunk {
meta.SetStatusCondition(&db.Status.Conditions, metav1.Condition{
Type: everestv1alpha1.ConditionTypeCannotResizeVolume,
Status: metav1.ConditionTrue,
Reason: everestv1alpha1.ReasonCannotShrinkVolume,
LastTransitionTime: metav1.Now(),
ObservedGeneration: db.GetGeneration(),
})
setStorageSizeFunc(currentSize)
return nil
}

// Check if storage size is being expanded. If not, set the desired size and return early.
hasStorageExpanded := currentSize.Cmp(desiredSize) < 0 && !currentSize.IsZero()
if !hasStorageExpanded {
setStorageSizeFunc(desiredSize)
return nil
}

allowedByStorageClass, err := storageClassSupportsVolumeExpansion(ctx, c, storageClass)
if err != nil {
return fmt.Errorf("failed to check if storage class supports volume expansion: %w", err)
}

if !allowedByStorageClass {
meta.SetStatusCondition(&db.Status.Conditions, metav1.Condition{
Type: everestv1alpha1.ConditionTypeCannotResizeVolume,
Status: metav1.ConditionTrue,
Reason: everestv1alpha1.ReasonStorageClassDoesNotSupportExpansion,
LastTransitionTime: metav1.Now(),
ObservedGeneration: db.GetGeneration(),
})
setStorageSizeFunc(currentSize)
return nil
}

setStorageSizeFunc(desiredSize)
return nil
}
Loading
Loading