Skip to content

Commit 2a7f656

Browse files
committed
Add inplace pvc resize controller
This controller will directly resize the PVCs if it's enabled and if the given cluster actually supports resizing PVCs on the fly.
1 parent 00b6452 commit 2a7f656

File tree

7 files changed

+132
-7
lines changed

7 files changed

+132
-7
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
cover.out
22
.github/release-notes.md
3+
.vscode
4+
__debug_bin*
35

46
# Binaries for programs and plugins
57
*.exe

controllers/inplace_controller.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package controllers
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"github.com/vshn/statefulset-resize-controller/statefulset"
8+
appsv1 "k8s.io/api/apps/v1"
9+
apierrors "k8s.io/apimachinery/pkg/api/errors"
10+
"k8s.io/apimachinery/pkg/runtime"
11+
"k8s.io/client-go/tools/record"
12+
ctrl "sigs.k8s.io/controller-runtime"
13+
"sigs.k8s.io/controller-runtime/pkg/client"
14+
"sigs.k8s.io/controller-runtime/pkg/log"
15+
)
16+
17+
// StatefulSetController is an interface for various implementations
18+
// of the StatefulSet controller.
19+
type StatefulSetController interface {
20+
SetupWithManager(ctrl.Manager) error
21+
}
22+
23+
// InplaceReconciler reconciles a StatefulSet object
24+
// It will resize the PVCs according to the sts template.
25+
type InplaceReconciler struct {
26+
client.Client
27+
Scheme *runtime.Scheme
28+
Recorder record.EventRecorder
29+
30+
RequeueAfter time.Duration
31+
LabelName string
32+
}
33+
34+
// Reconcile is the main work loop, reacting to changes in statefulsets and initiating resizing of StatefulSets.
35+
func (r *InplaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
36+
l := log.FromContext(ctx).WithValues("statefulset", req.NamespacedName)
37+
ctx = log.IntoContext(ctx, l)
38+
39+
sts := &appsv1.StatefulSet{}
40+
err := r.Client.Get(ctx, req.NamespacedName, sts)
41+
if err != nil {
42+
if apierrors.IsNotFound(err) {
43+
return ctrl.Result{}, nil
44+
}
45+
return ctrl.Result{}, err
46+
}
47+
48+
l.V(1).Info("Checking label for sts", "labelName", r.LabelName)
49+
if sts.GetLabels()[r.LabelName] != "true" {
50+
l.V(1).Info("Label not found, skipping sts")
51+
return ctrl.Result{}, nil
52+
}
53+
54+
l.Info("Found sts with label", "labelName", r.LabelName)
55+
56+
stsEntity, err := statefulset.NewEntity(sts)
57+
if err != nil {
58+
return ctrl.Result{}, err
59+
}
60+
61+
stsEntity.Pvcs, err = fetchResizablePVCs(ctx, r.Client, *stsEntity)
62+
if err != nil {
63+
return ctrl.Result{}, err
64+
}
65+
66+
if len(stsEntity.Pvcs) == 0 {
67+
l.Info("All PVCs have the right size")
68+
return ctrl.Result{}, nil
69+
}
70+
71+
return ctrl.Result{}, resizePVCsInplace(ctx, r.Client, stsEntity.Pvcs)
72+
}
73+
74+
// SetupWithManager sets up the controller with the Manager.
75+
func (r *InplaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
76+
return ctrl.NewControllerManagedBy(mgr).
77+
For(&appsv1.StatefulSet{}).
78+
Complete(r)
79+
}

controllers/pvc.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ import (
1616
)
1717

1818
// getResizablePVCs fetches the information of all PVCs that are smaller than the request of the statefulset
19-
func (r StatefulSetReconciler) fetchResizablePVCs(ctx context.Context, si statefulset.Entity) ([]pvc.Entity, error) {
19+
func fetchResizablePVCs(ctx context.Context, cl client.Client, si statefulset.Entity) ([]pvc.Entity, error) {
2020
// NOTE(glrf) This will get _all_ PVCs that belonged to the sts. Even the ones not used anymore (i.e. if scaled up and down).
2121
sts, err := si.StatefulSet()
2222
if err != nil {
2323
return nil, err
2424
}
2525
pvcs := corev1.PersistentVolumeClaimList{}
26-
if err := r.List(ctx, &pvcs, client.InNamespace(sts.Namespace), client.MatchingLabels(sts.Spec.Selector.MatchLabels)); err != nil {
26+
if err := cl.List(ctx, &pvcs, client.InNamespace(sts.Namespace), client.MatchingLabels(sts.Spec.Selector.MatchLabels)); err != nil {
2727
return nil, err
2828
}
2929
pis := filterResizablePVCs(ctx, *sts, pvcs.Items)
@@ -109,3 +109,22 @@ func (r *StatefulSetReconciler) resizePVCs(ctx context.Context, oldPIs []pvc.Ent
109109
}
110110
return pis, nil
111111
}
112+
113+
func resizePVCsInplace(ctx context.Context, cl client.Client, PVCs []pvc.Entity) error {
114+
l := log.FromContext(ctx)
115+
116+
for _, pvc := range PVCs {
117+
l.Info("Updating PVC", "PVCName", pvc.SourceName)
118+
119+
resizedPVC := pvc.GetResizedSource()
120+
resizedPVC.Spec.StorageClassName = pvc.SourceStorageClass
121+
resizedPVC.Spec.VolumeName = pvc.Spec.VolumeName
122+
123+
err := cl.Update(ctx, resizedPVC)
124+
if err != nil {
125+
return err
126+
}
127+
}
128+
129+
return nil
130+
}

controllers/statefulset.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func (r StatefulSetReconciler) fetchStatefulSet(ctx context.Context, namespacedN
5050
}
5151

5252
if !sts.Resizing() {
53-
sts.Pvcs, err = r.fetchResizablePVCs(ctx, *sts)
53+
sts.Pvcs, err = fetchResizablePVCs(ctx, r.Client, *sts)
5454
return sts, err
5555
}
5656
return sts, nil

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.16
44

55
require (
66
github.com/stretchr/testify v1.7.0
7+
go.uber.org/zap v1.18.1
78
k8s.io/api v0.21.3
89
k8s.io/apimachinery v0.21.3
910
k8s.io/client-go v0.21.3

main.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
99
// to ensure that exec-entrypoint and run can make use of them.
10+
"go.uber.org/zap/zapcore"
1011
_ "k8s.io/client-go/plugin/pkg/client/auth"
1112

1213
"k8s.io/apimachinery/pkg/runtime"
@@ -40,6 +41,9 @@ func main() {
4041
var probeAddr string
4142
var syncContainerImage string
4243
var syncClusterRole string
44+
var inplaceResize bool
45+
var inplaceLabelName string
46+
var logLevel int
4347
flag.StringVar(&syncContainerImage, "sync-image", "instrumentisto/rsync-ssh", "A container image containing rsync, used to move data.")
4448
flag.StringVar(&syncClusterRole, "sync-cluster-role", "", "ClusterRole to use for the sync jobs."+
4549
"For example, this can be used to allow the sync job to run as root on a cluster with PSPs enabled by providing the name of a ClusterRole which allows usage of a privileged PSP.")
@@ -48,11 +52,16 @@ func main() {
4852
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
4953
"Enable leader election for controller manager. "+
5054
"Enabling this will ensure there is only one active controller manager.")
55+
flag.BoolVar(&inplaceResize, "inplace", false, "Enable in-place update of PVCs. "+
56+
"If the underlying storage supports direct resizing of the PVCs this should be used.")
57+
flag.StringVar(&inplaceLabelName, "inplaceLabelName", "sts-resize.vshn.net/resize-inplace", "If inplace resize is enable the sts needs to have this label with value \"true\" in order to be handled.")
58+
flag.IntVar(&logLevel, "log-level", 0, "Set the log level.")
59+
flag.Parse()
60+
5161
opts := zap.Options{
5262
Development: true,
63+
Level: zapcore.Level(logLevel * -1),
5364
}
54-
opts.BindFlags(flag.CommandLine)
55-
flag.Parse()
5665

5766
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
5867

@@ -69,14 +78,26 @@ func main() {
6978
os.Exit(1)
7079
}
7180

72-
if err = (&controllers.StatefulSetReconciler{
81+
var stsController controllers.StatefulSetController = &controllers.StatefulSetReconciler{
7382
Client: mgr.GetClient(),
7483
Scheme: mgr.GetScheme(),
7584
Recorder: mgr.GetEventRecorderFor("statefulset-resize-controller"),
7685
SyncContainerImage: syncContainerImage,
7786
SyncClusterRole: syncClusterRole,
7887
RequeueAfter: 10 * time.Second,
79-
}).SetupWithManager(mgr); err != nil {
88+
}
89+
90+
if inplaceResize {
91+
stsController = &controllers.InplaceReconciler{
92+
Client: mgr.GetClient(),
93+
Scheme: mgr.GetScheme(),
94+
Recorder: mgr.GetEventRecorderFor("statefulset-resize-controller"),
95+
RequeueAfter: 10 * time.Second,
96+
LabelName: inplaceLabelName,
97+
}
98+
}
99+
100+
if err = stsController.SetupWithManager(mgr); err != nil {
80101
setupLog.Error(err, "unable to create controller", "controller", "StatefulSet")
81102
os.Exit(1)
82103
}

pvc/pvc.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ const ManagedLabel = "sts-resize.vshn.net/managed"
1515

1616
// NewEntity returns a new pvc Info
1717
func NewEntity(pvc corev1.PersistentVolumeClaim, growTo resource.Quantity, storageClassName *string) Entity {
18+
sourceStorageClassName := pvc.Spec.StorageClassName
1819
pvc.Spec.StorageClassName = storageClassName
1920
return Entity{
2021
SourceName: pvc.Name,
2122
Namespace: pvc.Namespace,
2223
Labels: pvc.Labels,
2324
TargetSize: growTo,
2425
TargetStorageClass: storageClassName,
26+
SourceStorageClass: sourceStorageClassName,
2527
Spec: pvc.Spec,
2628
}
2729
}
@@ -35,6 +37,7 @@ type Entity struct {
3537
Spec corev1.PersistentVolumeClaimSpec
3638
TargetSize resource.Quantity
3739
TargetStorageClass *string
40+
SourceStorageClass *string
3841

3942
BackedUp bool
4043
Restored bool

0 commit comments

Comments
 (0)