Skip to content

Commit c6b335f

Browse files
black-dragon74mergify[bot]
authored andcommitted
controller: add a new PVC controller - used with storageclass precedence
This patch adds a new controller for PVC objects which differs from existing controller in the following main ways: - Only supports annotations on StorageClasses - Sets us a silent watch on PV objects to optimize lookups. - Uses deterministic naming for child resources to avoid manual house keeping. Signed-off-by: Niraj Yadav <niryadav@redhat.com>
1 parent d06f425 commit c6b335f

File tree

1 file changed

+236
-0
lines changed

1 file changed

+236
-0
lines changed
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/*
2+
Copyright 2025 The Kubernetes-CSI-Addons Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controller
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
csiaddonsv1alpha1 "github.com/csi-addons/kubernetes-csi-addons/api/csiaddons/v1alpha1"
24+
"github.com/csi-addons/kubernetes-csi-addons/internal/connection"
25+
"github.com/csi-addons/kubernetes-csi-addons/internal/controller/utils"
26+
27+
"github.com/go-logr/logr"
28+
corev1 "k8s.io/api/core/v1"
29+
storagev1 "k8s.io/api/storage/v1"
30+
apierrors "k8s.io/apimachinery/pkg/api/errors"
31+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32+
"k8s.io/apimachinery/pkg/runtime"
33+
"k8s.io/apimachinery/pkg/types"
34+
ctrl "sigs.k8s.io/controller-runtime"
35+
"sigs.k8s.io/controller-runtime/pkg/builder"
36+
"sigs.k8s.io/controller-runtime/pkg/client"
37+
"sigs.k8s.io/controller-runtime/pkg/controller"
38+
"sigs.k8s.io/controller-runtime/pkg/handler"
39+
"sigs.k8s.io/controller-runtime/pkg/log"
40+
)
41+
42+
type PVCReconiler struct {
43+
client.Client
44+
Scheme *runtime.Scheme
45+
46+
// ConnectionPool consists of map of Connection objects.
47+
ConnPool *connection.ConnectionPool
48+
}
49+
50+
//+kubebuilder:rbac:groups=core,resources=persistentvolumeclaims,verbs=get;list;watch
51+
//+kubebuilder:rbac:groups=csiaddons.openshift.io,resources=reclaimspacecronjobs,verbs=get;list;watch;create;delete;update
52+
//+kubebuilder:rbac:groups=csiaddons.openshift.io,resources=encryptionkeyrotationcronjobs,verbs=get;list;watch;create;delete;update
53+
//+kubebuilder:rbac:groups=storage.k8s.io,resources=storageclasses,verbs=get;list;watch
54+
55+
func (r *PVCReconiler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
56+
logger := log.FromContext(ctx)
57+
58+
// Fetch the PVC
59+
pvc := &corev1.PersistentVolumeClaim{}
60+
if err := r.Get(ctx, req.NamespacedName, pvc); err != nil {
61+
return ctrl.Result{}, client.IgnoreNotFound(err)
62+
}
63+
64+
// Ignore if being deleted
65+
if !pvc.DeletionTimestamp.IsZero() {
66+
logger.Info("PVC is being deleted, exiting reconciliation", "PVCInfo", req.NamespacedName)
67+
68+
return ctrl.Result{}, nil
69+
}
70+
71+
logger.Info("Reconciling PVC", "PVCInfo", req.NamespacedName)
72+
73+
// The PVC must be in a bound state, if not, we requeue
74+
if pvc.Status.Phase != corev1.ClaimBound {
75+
logger.Info("PVC is not yet bound, requeue the request", "PVCInfo", req.NamespacedName)
76+
77+
return ctrl.Result{Requeue: true}, nil
78+
}
79+
80+
// Fetch the PV and check if it is CSI provisioned, if not, do nothing
81+
pv := &corev1.PersistentVolume{}
82+
if err := r.Get(ctx, types.NamespacedName{Name: pvc.Spec.VolumeName}, pv); err != nil {
83+
return ctrl.Result{}, client.IgnoreNotFound(err)
84+
}
85+
86+
// Must be CSI provisioned to continue
87+
if pv.Spec.CSI == nil {
88+
logger.Info("PVC is not CSI provisioned, exiting reconciliation", "PVCInfo", req.NamespacedName)
89+
90+
return ctrl.Result{}, nil
91+
}
92+
93+
// Should not be a static PVC
94+
if !hasValidStorageClassName(pvc) {
95+
logger.Info("The PVC is statically provisioned, exiting reconciliation", "PVCInfo", req.NamespacedName)
96+
97+
return ctrl.Result{}, nil
98+
}
99+
100+
// Now we fetch the StorageClass
101+
sc := &storagev1.StorageClass{}
102+
if err := r.Get(ctx, types.NamespacedName{Name: *pvc.Spec.StorageClassName}, sc); err != nil {
103+
return ctrl.Result{}, client.IgnoreNotFound(err)
104+
}
105+
106+
// Reconcile for dependent features
107+
// Reconcile - Key rotation
108+
keyRotationSched := sc.Annotations[utils.KrcJobScheduleTimeAnnotation]
109+
keyRotationEnabled := sc.Annotations[utils.KrEnableAnnotation]
110+
keyRotationChild := &csiaddonsv1alpha1.EncryptionKeyRotationCronJob{
111+
ObjectMeta: metav1.ObjectMeta{
112+
Name: fmt.Sprintf("%s-keyrotation", pvc.Name),
113+
Namespace: pvc.Namespace,
114+
},
115+
}
116+
117+
if err := r.reconcileFeature(ctx, logger, pvc, keyRotationChild, keyRotationSched, keyRotationEnabled); err != nil {
118+
return ctrl.Result{}, err
119+
}
120+
121+
// Reconcile - Reclaim space
122+
reclaimSpaceSched := sc.Annotations[utils.RsCronJobScheduleTimeAnnotation]
123+
relciamSpaceChild := &csiaddonsv1alpha1.ReclaimSpaceCronJob{
124+
ObjectMeta: metav1.ObjectMeta{
125+
Name: fmt.Sprintf("%s-reclaimspace", pvc.Name),
126+
Namespace: pvc.Namespace,
127+
},
128+
}
129+
130+
if err := r.reconcileFeature(ctx, logger, pvc, relciamSpaceChild, reclaimSpaceSched, "true"); err != nil {
131+
return ctrl.Result{}, err
132+
}
133+
134+
return ctrl.Result{}, nil
135+
}
136+
137+
func (r *PVCReconiler) reconcileFeature(
138+
ctx context.Context,
139+
logger logr.Logger,
140+
pvc *corev1.PersistentVolumeClaim,
141+
childObj client.Object,
142+
schedule string,
143+
enabledVal string,
144+
) error {
145+
defer logger.Info("Completed reconcile for feature")
146+
147+
// Determine if the object should exists in the cluster
148+
// Note: we do not return early if feature is disabled as we might need to garbage collect
149+
shouldExist := (schedule != "") && (enabledVal != "false")
150+
logger.Info("Reconciling feature for child object with values", "childObj", childObj, "schedule", schedule, "isEnabled", enabledVal, "shouldExist", shouldExist)
151+
152+
// Now check if it actually exists in the cluster
153+
exists := true
154+
existingObj := childObj.DeepCopyObject().(client.Object)
155+
if err := r.Get(ctx, types.NamespacedName{Name: childObj.GetName(), Namespace: childObj.GetNamespace()}, existingObj); err != nil {
156+
if apierrors.IsNotFound(err) {
157+
exists = false
158+
} else {
159+
return err
160+
}
161+
}
162+
logger.Info("determined the state to reconcile with", "shouldExist", shouldExist, "exists", exists)
163+
164+
// Object should not be present in the cluster, garbage collect or return
165+
if !shouldExist {
166+
if exists {
167+
logger.Info("Deleting the undersired object from the cluster", "childObj", existingObj)
168+
return client.IgnoreNotFound(r.Delete(ctx, existingObj))
169+
}
170+
171+
return nil
172+
}
173+
174+
// We reached here, means we need to either create or update the object in cluster
175+
// -- Create
176+
if !exists {
177+
// Set the required fields on the bare bones object
178+
utils.SetSpec(childObj, schedule, pvc.Name)
179+
180+
// Set controller reference
181+
if err := ctrl.SetControllerReference(pvc, childObj, r.Scheme); err != nil {
182+
return err
183+
}
184+
185+
logger.Info("creating a new object in the cluster", "newObj", childObj)
186+
187+
// We use deterministic names for the resources we create
188+
// This saves us additional listing and managing of stale resources
189+
return client.IgnoreAlreadyExists(r.Create(ctx, childObj))
190+
}
191+
192+
// -- Update
193+
currentSched := utils.GetSchedule(existingObj)
194+
if currentSched != schedule {
195+
// Update and set spec
196+
utils.SetSpec(existingObj, schedule, pvc.Name)
197+
198+
// Update the object in the cluster
199+
logger.Info("calling update for new schedule on object", "newObj", childObj, "currentSchedule", currentSched, "desiredSchedule", schedule)
200+
return r.Update(ctx, existingObj)
201+
}
202+
203+
return nil
204+
}
205+
206+
func (r *PVCReconiler) SetupWithManager(mgr ctrl.Manager, ctrlOptions controller.Options) error {
207+
// Setup the required indexers to optimize lookups
208+
if err := utils.SetupPVCControllerIndexers(mgr); err != nil {
209+
return err
210+
}
211+
212+
return ctrl.NewControllerManagedBy(mgr).
213+
// Primary source
214+
For(&corev1.PersistentVolumeClaim{}).
215+
216+
// Secondary sources
217+
Owns(&csiaddonsv1alpha1.ReclaimSpaceCronJob{}).
218+
Owns(&csiaddonsv1alpha1.EncryptionKeyRotationCronJob{}).
219+
220+
// Watch the storageclass and fan out according to predicates
221+
Watches(
222+
&storagev1.StorageClass{},
223+
handler.EnqueueRequestsFromMapFunc(utils.ScMapFunc(r.Client)),
224+
builder.WithPredicates(utils.StorageClassPredicate()),
225+
).
226+
227+
// Watch the PVs silently, this is to avoid client/server rate limits
228+
// And to make Get/List O(1)
229+
Watches(
230+
&corev1.PersistentVolume{},
231+
&handler.EnqueueRequestForObject{}, // This handler is never called due to `silentPredicate`
232+
builder.WithPredicates(utils.SilentPredicate()),
233+
).
234+
WithOptions(ctrlOptions).
235+
Complete(r)
236+
}

0 commit comments

Comments
 (0)