Skip to content

Commit 5908bc0

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 0b557d9 commit 5908bc0

File tree

6 files changed

+459
-6
lines changed

6 files changed

+459
-6
lines changed

apis/v1alpha1/config_types.go

Lines changed: 42 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
@@ -28,6 +29,8 @@ import (
2829

2930
// Config holds the configuration for bpfman-operator.
3031
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
32+
// +kubebuilder:printcolumn:name="Progressing",type="string",JSONPath=".status.conditions[?(@.type=='Progressing')].status"
33+
// +kubebuilder:printcolumn:name="Available",type="string",JSONPath=".status.conditions[?(@.type=='Available')].status"
3134
type Config struct {
3235
metav1.TypeMeta `json:",inline"`
3336
metav1.ObjectMeta `json:"metadata,omitempty"`
@@ -85,6 +88,10 @@ type ConfigStatus struct {
8588
// +listType=map
8689
// +listMapKey=type
8790
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
91+
92+
// componentStatuses stores the status of all components.
93+
// +optional
94+
ComponentStatuses *ConfigComponentStatuses `json:"componentStatuses,omitempty"`
8895
}
8996

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

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.
@@ -106,6 +111,14 @@ func (r *BpfmanConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request
106111
return ctrl.Result{}, err
107112
}
108113

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

127140
// Normal reconciliation - safe to create/update resources.
@@ -145,7 +158,7 @@ func (r *BpfmanConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request
145158
return ctrl.Result{}, err
146159
}
147160

148-
return ctrl.Result{}, nil
161+
return ctrl.Result{}, r.setStatusConditions(ctx, bpfmanConfig)
149162
}
150163

151164
func (r *BpfmanConfigReconciler) reconcileCM(ctx context.Context, bpfmanConfig *v1alpha1.Config) error {
@@ -225,6 +238,158 @@ func (r *BpfmanConfigReconciler) reconcileMetricsProxyDS(ctx context.Context, bp
225238
})
226239
}
227240

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

0 commit comments

Comments
 (0)