Skip to content

Commit

Permalink
Merge pull request #417 from cybozu-go/d-kuro/pvc-e2e
Browse files Browse the repository at this point in the history
Added e2e test to change pvc template
  • Loading branch information
masa213f authored Jul 4, 2022
2 parents 7cbca53 + 4f8e2d8 commit 21a21ce
Show file tree
Hide file tree
Showing 6 changed files with 347 additions and 10 deletions.
4 changes: 2 additions & 2 deletions controllers/mysqlcluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -953,9 +953,9 @@ func (r *MySQLClusterReconciler) reconcileV1StatefulSet(ctx context.Context, req
return false, err
}

return false, fmt.Errorf("re-creation failed the StatefulSet %s/%s has not been deleted", cluster.Namespace, cluster.PrefixedName())
return false, nil
}); err != nil {
return err
return fmt.Errorf("re-creation failed the StatefulSet %s/%s has not been deleted: %w", cluster.Namespace, cluster.PrefixedName(), err)
}
}
}
Expand Down
32 changes: 24 additions & 8 deletions controllers/pvc.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,22 @@ func (r *MySQLClusterReconciler) reconcilePVC(ctx context.Context, req ctrl.Requ

log.Info("Starting PVC resize")

if err := r.resizePVCs(ctx, cluster, &sts, resizeTarget); err != nil {
resized, err := r.resizePVCs(ctx, cluster, &sts, resizeTarget)
if err != nil {
metrics.VolumeResizedErrorTotal.WithLabelValues(cluster.Name, cluster.Namespace).Inc()
return err
}

if len(resized) == 0 {
return nil
}

metrics.VolumeResizedTotal.WithLabelValues(cluster.Name, cluster.Namespace).Inc()

return nil
}

func (r *MySQLClusterReconciler) resizePVCs(ctx context.Context, cluster *mocov1beta2.MySQLCluster, sts *appsv1.StatefulSet, resizeTarget map[string]corev1.PersistentVolumeClaim) error {
func (r *MySQLClusterReconciler) resizePVCs(ctx context.Context, cluster *mocov1beta2.MySQLCluster, sts *appsv1.StatefulSet, resizeTarget map[string]corev1.PersistentVolumeClaim) (map[string]corev1.PersistentVolumeClaim, error) {
log := crlog.FromContext(ctx)

newSizes := make(map[string]*resource.Quantity)
Expand Down Expand Up @@ -97,14 +102,16 @@ func (r *MySQLClusterReconciler) resizePVCs(ctx context.Context, cluster *mocov1

selector, err := metav1.LabelSelectorAsSelector(sts.Spec.Selector)
if err != nil {
return fmt.Errorf("failed to parse selector: %w", err)
return nil, fmt.Errorf("failed to parse selector: %w", err)
}

var pvcs corev1.PersistentVolumeClaimList
if err := r.Client.List(ctx, &pvcs, client.MatchingLabelsSelector{Selector: selector}); err != nil {
return fmt.Errorf("failed to list PVCs: %w", err)
return nil, fmt.Errorf("failed to list PVCs: %w", err)
}

resizedPVC := make(map[string]corev1.PersistentVolumeClaim)

for _, pvc := range pvcs.Items {
newSize, ok := pvcsToKeep[pvc.Name]
if !ok {
Expand All @@ -113,7 +120,7 @@ func (r *MySQLClusterReconciler) resizePVCs(ctx context.Context, cluster *mocov1

supported, err := r.isVolumeExpansionSupported(ctx, &pvc)
if err != nil {
return fmt.Errorf("failed to check if volume expansion is supported: %w", err)
return resizedPVC, fmt.Errorf("failed to check if volume expansion is supported: %w", err)
}
if !supported {
log.Info("StorageClass used by PVC does not support volume expansion, skipped", "storageClassName", *pvc.Spec.StorageClassName, "pvcName", pvc.Name)
Expand All @@ -124,17 +131,21 @@ func (r *MySQLClusterReconciler) resizePVCs(ctx context.Context, cluster *mocov1
case i == 0: // volume size is equal
continue
case i == 1: // current volume size is greater than new size
return fmt.Errorf("failed to resize pvc %q, want size: %s, deployed size: %s: %w", pvc.Name, newSize.String(), pvc.Spec.Resources.Requests.Storage().String(), ErrReduceVolumeSize)
return resizedPVC, fmt.Errorf("failed to resize pvc %q, want size: %s, deployed size: %s: %w", pvc.Name, newSize.String(), pvc.Spec.Resources.Requests.Storage().String(), ErrReduceVolumeSize)
case i == -1: // current volume size is smaller than new size
pvc.Spec.Resources.Requests[corev1.ResourceStorage] = *newSize

if err := r.Client.Update(ctx, &pvc); err != nil {
return fmt.Errorf("failed to update PVC: %w", err)
return resizedPVC, fmt.Errorf("failed to update PVC: %w", err)
}

log.Info("PVC resized", "pvcName", pvc.Name)

resizedPVC[pvc.Name] = pvc
}
}

return nil
return resizedPVC, nil
}

func (*MySQLClusterReconciler) needResizePVC(cluster *mocov1beta2.MySQLCluster, sts *appsv1.StatefulSet) (map[string]corev1.PersistentVolumeClaim, bool, error) {
Expand Down Expand Up @@ -197,6 +208,11 @@ func (r *MySQLClusterReconciler) isVolumeExpansionSupported(ctx context.Context,
// isUpdatingStatefulSet returns whether the StatefulSet is being updated or not.
// refs: https://github.com/kubernetes/kubectl/blob/v0.24.2/pkg/polymorphichelpers/rollout_status.go#L119-L152
func (*MySQLClusterReconciler) isUpdatingStatefulSet(sts *appsv1.StatefulSet) bool {
// Waiting for StatefulSet to be deleting.
if sts.DeletionTimestamp != nil {
return true
}

// Waiting for StatefulSet spec update to be observed
if sts.Status.ObservedGeneration == 0 || sts.Generation > sts.Status.ObservedGeneration {
return true
Expand Down
20 changes: 20 additions & 0 deletions e2e/mycluster.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
apiVersion: moco.cybozu.com/v1beta1
kind: MySQLCluster
metadata:
namespace: default
name: test
spec:
replicas: 3
podTemplate:
spec:
containers:
- name: mysqld
image: quay.io/cybozu/mysql:8.0.28
volumeClaimTemplates:
- metadata:
name: mysql-data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
197 changes: 197 additions & 0 deletions e2e/pvc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package e2e

import (
"bytes"
_ "embed"
"encoding/json"
"errors"
"fmt"
"reflect"

mocov1beta2 "github.com/cybozu-go/moco/api/v1beta2"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/prometheus/common/expfmt"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
)

//go:embed testdata/pvc_test.yaml
var pvcTestYAML string

//go:embed testdata/pvc_test_changed.yaml
var pvcApplyYAML string

var _ = Context("pvc_test", func() {

It("should construct a cluster", func() {
kubectlSafe(fillTemplate(pvcTestYAML), "apply", "-f", "-")
Eventually(func() error {
cluster, err := getCluster("pvc", "cluster")
if err != nil {
return err
}
for _, cond := range cluster.Status.Conditions {
if cond.Type != mocov1beta2.ConditionHealthy {
continue
}
if cond.Status == corev1.ConditionTrue {
return nil
}
return fmt.Errorf("cluster is not healthy: %s", cond.Status)
}
return errors.New("no health condition")
}).Should(Succeed())
})

It("should pvc template change succeed", func() {
kubectlSafe(fillTemplate(pvcApplyYAML), "apply", "-f", "-")
Eventually(func() error {
cluster, err := getCluster("pvc", "cluster")
if err != nil {
return err
}
for _, cond := range cluster.Status.Conditions {
if cond.Type != mocov1beta2.ConditionHealthy {
continue
}
if cond.Status == corev1.ConditionTrue {
return nil
}
return fmt.Errorf("cluster is not healthy: %s", cond.Status)
}
return errors.New("no health condition")
}).Should(Succeed())
})

It("should statefulset re-created", func() {
cluster, err := getCluster("pvc", "cluster")
Expect(err).NotTo(HaveOccurred())

wantLabels := make(map[string]map[string]string)
for _, pvc := range cluster.Spec.VolumeClaimTemplates {
wantLabels[pvc.Name] = pvc.ObjectMeta.Labels
}

wantSizes := make(map[string]*resource.Quantity)
for _, pvc := range cluster.Spec.VolumeClaimTemplates {
wantSizes[pvc.Name] = pvc.Spec.Resources.Requests.Storage()
}

Eventually(func() error {
out, err := kubectl(nil,
"get", "sts",
"-n", "pvc",
"moco-cluster",
"-o", "json",
)
if err != nil {
return err
}

var sts appsv1.StatefulSet
if err := json.Unmarshal(out, &sts); err != nil {
return err
}

for _, pvc := range sts.Spec.VolumeClaimTemplates {
labels, ok := wantLabels[pvc.Name]
if !ok {
return fmt.Errorf("pvc %s is not expected", pvc.Name)
}

if !reflect.DeepEqual(pvc.ObjectMeta.Labels, labels) {
return fmt.Errorf("pvc %s labels are not expected", pvc.Name)
}

want, ok := wantSizes[pvc.Name]
if !ok {
return fmt.Errorf("pvc %s is not expected", pvc.Name)
}

if pvc.Spec.Resources.Requests.Storage().Cmp(*want) != 0 {
return fmt.Errorf("pvc %s is not expected size: %s", pvc.Name, pvc.Spec.Resources.Requests.Storage())
}
}

return nil
}).Should(Succeed())
})

It("should pvc resized", func() {
cluster, err := getCluster("pvc", "cluster")
Expect(err).NotTo(HaveOccurred())

wantSizes := make(map[string]*resource.Quantity)
for _, pvc := range cluster.Spec.VolumeClaimTemplates {
for i := int32(0); i < cluster.Spec.Replicas; i++ {
name := fmt.Sprintf("%s-%s-%d", pvc.Name, "moco-cluster", i)
wantSizes[name] = pvc.Spec.Resources.Requests.Storage()
}
}

Eventually(func() error {
out, err := kubectl(nil,
"get", "pvc",
"-n", "pvc",
"-l", "app.kubernetes.io/instance=cluster",
"-o", "json",
)
if err != nil {
return err
}

var pvcList corev1.PersistentVolumeClaimList
if err := json.Unmarshal(out, &pvcList); err != nil {
return err
}
if len(pvcList.Items) < 1 {
return errors.New("not found pvcs")
}

if len(pvcList.Items) != len(wantSizes) {
return fmt.Errorf("pvc count is not expected: %d", len(pvcList.Items))
}

for _, pvc := range pvcList.Items {
want, ok := wantSizes[pvc.Name]
if !ok {
return fmt.Errorf("pvc %s is not expected", pvc.Name)
}

if pvc.Spec.Resources.Requests.Storage().Cmp(*want) != 0 {
return fmt.Errorf("pvc %s is not expected size: %s", pvc.Name, pvc.Spec.Resources.Requests.Storage())
}
}

return nil
}).Should(Succeed())
})

It("metrics", func() {
out := kubectlSafe(nil, "-n", "moco-system", "get", "pods", "-l", "app.kubernetes.io/component=moco-controller", "-o", "json")
pods := &corev1.PodList{}
err := json.Unmarshal(out, pods)
Expect(err).NotTo(HaveOccurred())
Expect(pods.Items).To(HaveLen(1))
addr := pods.Items[0].Status.PodIP
out, err = runInPod("curl", "-sf", fmt.Sprintf("http://%s:8080/metrics", addr))
Expect(err).NotTo(HaveOccurred())

mfs, err := (&expfmt.TextParser{}).TextToMetricFamilies(bytes.NewReader(out))
Expect(err).NotTo(HaveOccurred())

volumeMf := mfs["moco_cluster_volume_resized_total"]
Expect(volumeMf).NotTo(BeNil())
volumeMetric := findMetric(volumeMf, map[string]string{"namespace": "pvc", "name": "cluster"})
Expect(volumeMetric).NotTo(BeNil())
Expect(volumeMetric.GetCounter().GetValue()).To(BeNumerically("==", 1))

stsMf := mfs["moco_cluster_statefulset_recreate_total"]
Expect(stsMf).NotTo(BeNil())
stsMetric := findMetric(stsMf, map[string]string{"namespace": "pvc", "name": "cluster"})
Expect(stsMetric).NotTo(BeNil())
Expect(stsMetric.GetCounter().GetValue()).To(BeNumerically("==", 1))
})
})
62 changes: 62 additions & 0 deletions e2e/testdata/pvc_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
apiVersion: v1
kind: Namespace
metadata:
name: pvc
---
apiVersion: v1
kind: ConfigMap
metadata:
namespace: pvc
name: mycnf
data:
innodb_log_file_size: "10M"
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard-allow-volume-expansion
provisioner: rancher.io/local-path
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
---
apiVersion: moco.cybozu.com/v1beta2
kind: MySQLCluster
metadata:
namespace: pvc
name: cluster
spec:
replicas: 1
mysqlConfigMapName: mycnf
podTemplate:
spec:
containers:
- name: mysqld
image: quay.io/cybozu/mysql:{{ . }}
# Specify minimum resources so as not to overwhelm CI resources.
overwriteContainers:
- name: agent
resources:
requests:
cpu: 1m
- name: moco-init
resources:
requests:
cpu: 1m
- name: slow-log
resources:
requests:
cpu: 1m
- name: mysqld-exporter
resources:
requests:
cpu: 1m
volumeClaimTemplates:
- metadata:
name: mysql-data
spec:
storageClassName: standard-allow-volume-expansion
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 500Mi
Loading

0 comments on commit 21a21ce

Please sign in to comment.