Skip to content

Commit 3106ff2

Browse files
committed
Add status tracking to Config resource
Implements status conditions and component status tracking for the Config custom resource to provide visibility into the reconciliation state of managed components (ConfigMap, DaemonSets, CSIDriver, SCC). - Add ConfigComponentStatuses type with status for each component - Implement Progressing and Available conditions following Kubernetes conventions - Add event recording for status changes - Update controller with status subresource RBAC permissions - Add test coverage for all status states - Integrate status checks into lifecycle tests The status system tracks individual component readiness and sets overall conditions to indicate when the Config is progressing or fully available. Signed-off-by: Andreas Karis <[email protected]>
1 parent a0212de commit 3106ff2

File tree

6 files changed

+457
-6
lines changed

6 files changed

+457
-6
lines changed

apis/v1alpha1/config_types.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package v1alpha1
1818

1919
import (
2020
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+
"k8s.io/utils/ptr"
2122
)
2223

2324
// +genclient
@@ -85,6 +86,10 @@ type ConfigStatus struct {
8586
// +listType=map
8687
// +listMapKey=type
8788
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
89+
90+
// componentStatuses stores the status of all components.
91+
// +optional
92+
ComponentStatuses *ConfigComponentStatuses `json:"componentStatuses,omitempty"`
8893
}
8994

9095
// +kubebuilder:object:root=true
@@ -94,3 +99,38 @@ type ConfigList struct {
9499
metav1.ListMeta `json:"metadata,omitempty"`
95100
Items []Config `json:"items"`
96101
}
102+
103+
type ConfigComponentStatus string
104+
105+
const (
106+
ConfigStatusUnknown ConfigComponentStatus = "Unknown"
107+
ConfigStatusProgressing ConfigComponentStatus = "Progressing"
108+
ConfigStatusReady ConfigComponentStatus = "Ready"
109+
)
110+
111+
type ConfigComponentStatuses struct {
112+
// configMap stores the status of the ConfigMap.
113+
// +optional
114+
ConfigMap *ConfigComponentStatus `json:"configMap,omitempty"`
115+
// daemonSet stores the status of the DaemonSet.
116+
// +optional
117+
DaemonSet *ConfigComponentStatus `json:"daemonSet,omitempty"`
118+
// metricsProxyDaemonSet stores the status of the MetricsProxyDaemonSet.
119+
// +optional
120+
MetricsProxyDaemonSet *ConfigComponentStatus `json:"metricsProxyDaemonSet,omitempty"`
121+
// csiDriver stores the status of the CsiDriver.
122+
// +optional
123+
CsiDriver *ConfigComponentStatus `json:"csiDriver,omitempty"`
124+
// scc stores the status of the Scc.
125+
// +optional
126+
Scc *ConfigComponentStatus `json:"scc,omitempty"`
127+
}
128+
129+
// Equals compares two ConfigComponentStatuses instances for equality.
130+
func (ccs ConfigComponentStatuses) Equals(c ConfigComponentStatuses) bool {
131+
return ptr.Equal(ccs.ConfigMap, c.ConfigMap) &&
132+
ptr.Equal(ccs.DaemonSet, c.DaemonSet) &&
133+
ptr.Equal(ccs.MetricsProxyDaemonSet, c.MetricsProxyDaemonSet) &&
134+
ptr.Equal(ccs.CsiDriver, c.CsiDriver) &&
135+
ptr.Equal(ccs.Scc, c.Scc)
136+
}

cmd/bpfman-operator/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ func main() {
201201
CsiDriverDS: internal.BpfmanCsiDriverPath,
202202
RestrictedSCC: internal.BpfmanRestrictedSCCPath,
203203
IsOpenshift: isOpenshift,
204+
Recorder: mgr.GetEventRecorderFor("config-controller"),
204205
}).SetupWithManager(mgr); err != nil {
205206
setupLog.Error(err, "unable to create bpfmanConfig controller")
206207
os.Exit(1)

controllers/bpfman-operator/config.go

Lines changed: 167 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@ import (
2929
storagev1 "k8s.io/api/storage/v1"
3030
"k8s.io/apimachinery/pkg/api/equality"
3131
"k8s.io/apimachinery/pkg/api/errors"
32+
meta "k8s.io/apimachinery/pkg/api/meta"
3233
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3334
"k8s.io/apimachinery/pkg/types"
3435
"k8s.io/client-go/kubernetes/scheme"
36+
"k8s.io/client-go/tools/record"
3537
"k8s.io/utils/ptr"
3638
ctrl "sigs.k8s.io/controller-runtime"
3739
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -51,7 +53,9 @@ import (
5153
// +kubebuilder:rbac:groups=storage.k8s.io,resources=csidrivers,verbs=get;list;watch;create;update;patch;delete
5254
// +kubebuilder:rbac:groups=security.openshift.io,resources=securitycontextconstraints,verbs=get;list;watch;create;update;patch;delete
5355
// +kubebuilder:rbac:groups=bpfman.io,resources=configs,verbs=get;list;watch;update;patch;delete
56+
// +kubebuilder:rbac:groups=bpfman.io,resources=configs/status,verbs=get;update;patch
5457
// +kubebuilder:rbac:groups=bpfman.io,resources=configs/finalizers,verbs=update
58+
// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
5559

5660
type BpfmanConfigReconciler struct {
5761
ClusterApplicationReconciler
@@ -60,6 +64,7 @@ type BpfmanConfigReconciler struct {
6064
CsiDriverDS string
6165
RestrictedSCC string
6266
IsOpenshift bool
67+
Recorder record.EventRecorder
6368
}
6469

6570
// SetupWithManager sets up the controller with the Manager.
@@ -104,6 +109,14 @@ func (r *BpfmanConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request
104109
return ctrl.Result{}, err
105110
}
106111

112+
// If we haven't added any conditions, yet, set them to unknown.
113+
if len(bpfmanConfig.Status.Conditions) == 0 {
114+
r.Logger.Info("Adding initial status conditions", "name", bpfmanConfig.Name)
115+
if err := r.setStatusConditions(ctx, bpfmanConfig); err != nil {
116+
return ctrl.Result{}, err
117+
}
118+
}
119+
107120
// Check if Config is being deleted first to prevent race
108121
// conditions.
109122
if !bpfmanConfig.DeletionTimestamp.IsZero() {
@@ -119,7 +132,7 @@ func (r *BpfmanConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request
119132
r.Logger.Error(err, "Failed to add finalizer to Config")
120133
return ctrl.Result{}, err
121134
}
122-
return ctrl.Result{}, nil
135+
return ctrl.Result{}, r.setStatusConditions(ctx, bpfmanConfig)
123136
}
124137

125138
// Normal reconciliation - safe to create/update resources.
@@ -143,7 +156,7 @@ func (r *BpfmanConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request
143156
return ctrl.Result{}, err
144157
}
145158

146-
return ctrl.Result{}, nil
159+
return ctrl.Result{}, r.setStatusConditions(ctx, bpfmanConfig)
147160
}
148161

149162
func (r *BpfmanConfigReconciler) reconcileCM(ctx context.Context, bpfmanConfig *v1alpha1.Config) error {
@@ -223,6 +236,158 @@ func (r *BpfmanConfigReconciler) reconcileMetricsProxyDS(ctx context.Context, bp
223236
})
224237
}
225238

239+
// setStatusConditions checks the status of all Config components (ConfigMap, DaemonSets, CSIDriver, SCC)
240+
// and updates the Config's status.componentStatuses and status.conditions accordingly.
241+
// It also emits Kubernetes events for status changes and re-fetches the Config object after updating.
242+
func (r *BpfmanConfigReconciler) setStatusConditions(ctx context.Context, config *v1alpha1.Config) error {
243+
if r == nil {
244+
return fmt.Errorf("object BpfmanConfigReconciler r is nil")
245+
}
246+
if config == nil {
247+
return fmt.Errorf("object Config config is nil")
248+
}
249+
if r.Recorder == nil {
250+
return fmt.Errorf("object Recorder is nil")
251+
}
252+
253+
// Check each resource and set appropriate status.
254+
statuses := v1alpha1.ConfigComponentStatuses{}
255+
256+
cm := &corev1.ConfigMap{}
257+
if err := r.Get(ctx, types.NamespacedName{
258+
Name: internal.BpfmanCmName, Namespace: config.Spec.Namespace}, cm); err != nil {
259+
if errors.IsNotFound(err) {
260+
statuses.ConfigMap = ptr.To(v1alpha1.ConfigStatusUnknown)
261+
} else {
262+
return err
263+
}
264+
} else {
265+
statuses.ConfigMap = ptr.To(v1alpha1.ConfigStatusReady)
266+
}
267+
268+
ds := &appsv1.DaemonSet{}
269+
if err := r.Get(ctx, types.NamespacedName{
270+
Name: internal.BpfmanDsName, Namespace: config.Spec.Namespace}, ds); err != nil {
271+
if errors.IsNotFound(err) {
272+
statuses.DaemonSet = ptr.To(v1alpha1.ConfigStatusUnknown)
273+
} else {
274+
return err
275+
}
276+
} else {
277+
if ds.Status.NumberReady == ds.Status.DesiredNumberScheduled && ds.Status.DesiredNumberScheduled > 0 {
278+
statuses.DaemonSet = ptr.To(v1alpha1.ConfigStatusReady)
279+
} else {
280+
statuses.DaemonSet = ptr.To(v1alpha1.ConfigStatusProgressing)
281+
}
282+
}
283+
284+
metricsDS := &appsv1.DaemonSet{}
285+
if err := r.Get(ctx, types.NamespacedName{
286+
Name: internal.BpfmanMetricsProxyDsName, Namespace: config.Spec.Namespace}, metricsDS); err != nil {
287+
if errors.IsNotFound(err) {
288+
statuses.MetricsProxyDaemonSet = ptr.To(v1alpha1.ConfigStatusUnknown)
289+
} else {
290+
return err
291+
}
292+
} else {
293+
if metricsDS.Status.NumberReady == metricsDS.Status.DesiredNumberScheduled &&
294+
metricsDS.Status.DesiredNumberScheduled > 0 {
295+
statuses.MetricsProxyDaemonSet = ptr.To(v1alpha1.ConfigStatusReady)
296+
} else {
297+
statuses.MetricsProxyDaemonSet = ptr.To(v1alpha1.ConfigStatusProgressing)
298+
}
299+
}
300+
301+
csiDriver := &storagev1.CSIDriver{}
302+
if err := r.Get(ctx, types.NamespacedName{Name: internal.BpfmanCsiDriverName}, csiDriver); err != nil {
303+
if errors.IsNotFound(err) {
304+
statuses.CsiDriver = ptr.To(v1alpha1.ConfigStatusUnknown)
305+
} else {
306+
return err
307+
}
308+
} else {
309+
statuses.CsiDriver = ptr.To(v1alpha1.ConfigStatusReady)
310+
}
311+
312+
if r.IsOpenshift {
313+
scc := &osv1.SecurityContextConstraints{}
314+
if err := r.Get(ctx, types.NamespacedName{Name: internal.BpfmanRestrictedSccName}, scc); err != nil {
315+
if errors.IsNotFound(err) {
316+
statuses.Scc = ptr.To(v1alpha1.ConfigStatusUnknown)
317+
} else {
318+
return err
319+
}
320+
} else {
321+
statuses.Scc = ptr.To(v1alpha1.ConfigStatusReady)
322+
}
323+
}
324+
325+
// Set component statuses, first.
326+
config.Status.ComponentStatuses = &statuses
327+
328+
// Set conditions, next.
329+
switch {
330+
case statuses.ConfigMap != nil && *statuses.ConfigMap == v1alpha1.ConfigStatusProgressing ||
331+
statuses.DaemonSet != nil && *statuses.DaemonSet == v1alpha1.ConfigStatusProgressing ||
332+
statuses.MetricsProxyDaemonSet != nil && *statuses.MetricsProxyDaemonSet == v1alpha1.ConfigStatusProgressing ||
333+
statuses.CsiDriver != nil && *statuses.CsiDriver == v1alpha1.ConfigStatusProgressing ||
334+
(r.IsOpenshift && statuses.Scc != nil && *statuses.Scc == v1alpha1.ConfigStatusProgressing):
335+
meta.SetStatusCondition(&config.Status.Conditions, metav1.Condition{
336+
Type: internal.ConfigConditionProgressing,
337+
Status: metav1.ConditionTrue,
338+
Reason: internal.ConfigReasonProgressing,
339+
Message: internal.ConfigMessageProgressing,
340+
})
341+
meta.SetStatusCondition(&config.Status.Conditions, metav1.Condition{
342+
Type: internal.ConfigConditionAvailable,
343+
Status: metav1.ConditionFalse,
344+
Reason: internal.ConfigReasonProgressing,
345+
Message: internal.ConfigMessageProgressing,
346+
})
347+
r.Recorder.Event(config, "Normal", internal.ConfigReasonProgressing, internal.ConfigMessageProgressing)
348+
case statuses.ConfigMap != nil && *statuses.ConfigMap == v1alpha1.ConfigStatusReady &&
349+
statuses.DaemonSet != nil && *statuses.DaemonSet == v1alpha1.ConfigStatusReady &&
350+
statuses.MetricsProxyDaemonSet != nil && *statuses.MetricsProxyDaemonSet == v1alpha1.ConfigStatusReady &&
351+
statuses.CsiDriver != nil && *statuses.CsiDriver == v1alpha1.ConfigStatusReady &&
352+
(!r.IsOpenshift || statuses.Scc != nil && *statuses.Scc == v1alpha1.ConfigStatusReady):
353+
meta.SetStatusCondition(&config.Status.Conditions, metav1.Condition{
354+
Type: internal.ConfigConditionProgressing,
355+
Status: metav1.ConditionFalse,
356+
Reason: internal.ConfigReasonAvailable,
357+
Message: internal.ConfigMessageAvailable,
358+
})
359+
meta.SetStatusCondition(&config.Status.Conditions, metav1.Condition{
360+
Type: internal.ConfigConditionAvailable,
361+
Status: metav1.ConditionTrue,
362+
Reason: internal.ConfigReasonAvailable,
363+
Message: internal.ConfigMessageAvailable,
364+
})
365+
r.Recorder.Event(config, "Normal", internal.ConfigReasonAvailable, internal.ConfigMessageAvailable)
366+
default:
367+
meta.SetStatusCondition(&config.Status.Conditions, metav1.Condition{
368+
Type: internal.ConfigConditionProgressing,
369+
Status: metav1.ConditionUnknown,
370+
Reason: internal.ConfigReasonUnknown,
371+
Message: internal.ConfigMessageUnknown,
372+
})
373+
meta.SetStatusCondition(&config.Status.Conditions, metav1.Condition{
374+
Type: internal.ConfigConditionAvailable,
375+
Status: metav1.ConditionUnknown,
376+
Reason: internal.ConfigReasonUnknown,
377+
Message: internal.ConfigMessageUnknown,
378+
})
379+
r.Recorder.Event(config, "Normal", internal.ConfigReasonUnknown, internal.ConfigMessageUnknown)
380+
}
381+
382+
// Update the status.
383+
if err := r.Status().Update(ctx, config); err != nil {
384+
return fmt.Errorf("cannot update status for config %q, err: %w", config.Name, err)
385+
}
386+
387+
// Update the object again (config is a pointer).
388+
return r.Get(ctx, types.NamespacedName{Name: config.Name}, config)
389+
}
390+
226391
// resourcePredicate creates a predicate that filters events based on resource name.
227392
// Only processes events for resources matching the specified resourceName.
228393
func resourcePredicate(resourceName string) predicate.Funcs {

0 commit comments

Comments
 (0)