Skip to content

Commit 6bec367

Browse files
committed
[CONTP-1610][CONTP-1611] Wait for CSI driver node server pod readiness in untaint controller if csi feature is enabled
1 parent 39d6bd3 commit 6bec367

11 files changed

Lines changed: 488 additions & 127 deletions

File tree

docs/untaint_controller.md

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,42 @@ This feature was introduced in Datadog Operator v1.28 and is currently in previe
55
## Overview
66

77
The Untaint controller watches Kubernetes Nodes carrying the taint
8-
`agent.datadoghq.com/not-ready=presence:NoSchedule` and removes it once the
9-
Datadog Agent pod on that node is `Ready`. It is intended to run alongside a
10-
separate mechanism (cluster-autoscaler hook, CCM, admission webhook, etc.)
11-
that adds the taint to new nodes. The use case is keeping workloads off a
12-
node until the Datadog Agent is Ready, and recovering gracefully if the Agent never
13-
becomes Ready.
14-
15-
Agent pods are matched by the label `agent.datadoghq.com/component=agent` in
16-
the operator's watched namespaces (`WATCH_NAMESPACE` /
8+
`agent.datadoghq.com/not-ready=presence:NoSchedule` and removes it when
9+
readiness criteria are met (see below), or after a configurable timeout. It is
10+
intended to run alongside a separate mechanism (cluster-autoscaler hook, CCM,
11+
admission webhook, etc.) that adds the taint to new nodes.
12+
13+
**With `--untaintControllerEnabled` only** (or with `--datadogCSIDriverEnabled=false`):
14+
the controller removes the taint once the **node Agent** pod
15+
(`agent.datadoghq.com/component=agent`) on that node is `Ready`. Agent pods are
16+
listed in the operator's agent watch namespaces (`WATCH_NAMESPACE` /
1717
`DD_AGENT_WATCH_NAMESPACE`).
1818

19-
If the Agent pod never reaches Ready on a tainted node, a configurable timeout
20-
policy ensures the node is never permanently unschedulable. Two clocks cover
21-
the two failure modes:
19+
**With both `--untaintControllerEnabled=true` and `--datadogCSIDriverEnabled=true`:**
20+
the controller waits until **both** the node Agent and **CSI
21+
node-server** pod (`app=datadog-csi-driver-node-server`) on the node are
22+
`Ready` before removing the taint. The taint stays until both are
23+
satisfied or a timeout fires. The operator's Pod informer then watches the
24+
**union** of `DD_AGENT_WATCH_NAMESPACE` and `DD_CSIDRIVER_WATCH_NAMESPACE` (all
25+
pods in those namespaces—keep namespaces tight). Ensure CSI namespaces are
26+
covered so the controller can list CSI pod status.
2227

23-
- **Readiness timeout** — the Agent pod is on the node but not Ready. Clock:
24-
`pod.Status.StartTime`. Pod recreation restarts the window; container
25-
restarts inside the same pod do not.
26-
- **Scheduling timeout** — no Agent pod is on the node. Clock:
27-
`node.metadata.creationTimestamp`. The expected path when a DaemonSet never
28-
schedules a pod onto the node (taint not tolerated, missing labels, etc.).
28+
If a required pod never reaches Ready on a tainted node, a configurable timeout
29+
policy ensures the node is never permanently unschedulable. Two clocks cover
30+
the main failure modes:
31+
32+
- **Readiness timeout** — at least one Agent pod is on the node but the Agent
33+
is not Ready yet, **or** (with CSI enabled) the Agent is Ready but a CSI
34+
node-server pod exists on the node and is not Ready. Clock: latest
35+
`pod.Status.StartTime` among **Agent** pods in the first case, and among **CSI
36+
node-server** pods only in the second (the Agent’s age does not shorten the
37+
wait for CSI). Pod recreation restarts the window; container restarts inside the
38+
same pod do not.
39+
- **Scheduling timeout** — no Agent pod is on the node, **or** (with CSI
40+
enabled) the Agent is Ready but **no** CSI node-server pod is on the node
41+
yet. Clock: `node.metadata.creationTimestamp`. Covers DaemonSets that never
42+
schedule onto the node (taint not tolerated, missing labels, CSI still pulling,
43+
etc.).
2944

3045
A pod-recreation crash-loop faster than the readiness window can hold a node
3146
tainted indefinitely; run with `policy=keep` and alert on
@@ -49,12 +64,19 @@ args:
4964
- --untaintControllerEnabled=true
5065
```
5166
52-
When this flag is enabled, the operator also injects a toleration for
67+
| `--untaintControllerEnabled` | `--datadogCSIDriverEnabled` | Behavior |
68+
| ----------------------------- | --------------------------- | -------- |
69+
| `false` | any | Untaint controller off; no startup toleration injection for this feature on Agent or CSI. |
70+
| `true` | `false` | Agent-only readiness and Agent DaemonSet toleration (default historical behavior). |
71+
| `true` | `true` | Wait for Agent **and** CSI node-server Ready; widened Pod cache (agent + `DD_CSIDRIVER_WATCH_NAMESPACE` namespaces); toleration on Agent and CSI DaemonSets. |
72+
73+
When this flag is enabled, the operator injects a toleration for
5374
`agent.datadoghq.com/not-ready=presence:NoSchedule` into the node Agent
5475
DaemonSet (or ExtendedDaemonSet) pod template, unless an equivalent toleration
55-
is already present. This avoids a deadlock where the node stays tainted because
56-
the Agent pod cannot schedule without the toleration, especially when admission
57-
webhook auto-injection is not in use.
76+
is already present. When **`--datadogCSIDriverEnabled`** is also true, the same
77+
toleration is injected into the **Datadog CSI node-server** DaemonSet pod
78+
template so the CSI workload can schedule on tainted nodes before the taint is
79+
removed.
5880

5981
## Configuration
6082

@@ -81,6 +103,8 @@ Metrics, under the `untaint` Prometheus subsystem:
81103

82104
Kubernetes Events (gated by `DD_UNTAINT_CONTROLLER_EVENTS_ENABLED=true`):
83105

84-
- `TaintRemoved` (Normal) — taint removed because the Agent pod became Ready.
106+
- `TaintRemoved` (Normal) — taint removed after the Agent became Ready, or (when
107+
the Datadog CSI driver controller is also enabled) after both the Agent and
108+
CSI node-server pods became Ready.
85109
- `UntaintTimeout` — a timeout fired. Normal under `remove`, Warning under `keep`. Message carries the reason, elapsed time, and policy.
86110

internal/controller/datadogcsidriver/const.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@
66
package datadogcsidriver
77

88
const (
9+
// AppLabelKey is the Kubernetes label key on CSI node-server pods.
10+
AppLabelKey = "app"
11+
// NodeServerDaemonSetAppValue is the label value identifying CSI node-server pods
12+
// (and the default DaemonSet name).
13+
NodeServerDaemonSetAppValue = "datadog-csi-driver-node-server"
14+
915
// csiDsName is the default name of the CSI driver DaemonSet
10-
csiDsName = "datadog-csi-driver-node-server"
16+
csiDsName = NodeServerDaemonSetAppValue
1117
// csiDriverName is the default name of the CSIDriver Kubernetes object
1218
csiDriverName = "k8s.csi.datadoghq.com"
1319
// defaultCSIDriverImageName is the default CSI driver container image name
@@ -51,7 +57,6 @@ const (
5157
csiDriverPort = int32(5000)
5258

5359
// Pod labels
54-
appLabelKey = "app"
5560
admissionControllerEnabledLabel = "admission.datadoghq.com/enabled"
5661

5762
// finalizerName is the finalizer for CSIDriver object cleanup

internal/controller/datadogcsidriver/controller.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626

2727
"github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1"
2828
"github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1"
29+
componentagent "github.com/DataDog/datadog-operator/internal/controller/datadogagent/component/agent"
2930
)
3031

3132
const (
@@ -35,17 +36,19 @@ const (
3536

3637
// Reconciler reconciles a DatadogCSIDriver object
3738
type Reconciler struct {
38-
client client.Client
39-
scheme *runtime.Scheme
40-
recorder record.EventRecorder
39+
client client.Client
40+
scheme *runtime.Scheme
41+
recorder record.EventRecorder
42+
untaintControllerEnabled bool
4143
}
4244

4345
// NewReconciler creates a new DatadogCSIDriver reconciler
44-
func NewReconciler(client client.Client, scheme *runtime.Scheme, recorder record.EventRecorder) *Reconciler {
46+
func NewReconciler(client client.Client, scheme *runtime.Scheme, recorder record.EventRecorder, untaintControllerEnabled bool) *Reconciler {
4547
return &Reconciler{
46-
client: client,
47-
scheme: scheme,
48-
recorder: recorder,
48+
client: client,
49+
scheme: scheme,
50+
recorder: recorder,
51+
untaintControllerEnabled: untaintControllerEnabled,
4952
}
5053
}
5154

@@ -198,13 +201,16 @@ func (r *Reconciler) reconcileCSIDriver(ctx context.Context, instance *v1alpha1.
198201
}
199202

200203
func (r *Reconciler) reconcileDaemonSet(ctx context.Context, instance *v1alpha1.DatadogCSIDriver) error {
204+
logger := ctrl.LoggerFrom(ctx)
201205
desired := buildDaemonSet(instance)
206+
if r.untaintControllerEnabled {
207+
componentagent.EnsureAgentNotReadyStartupToleration(logger, &desired.Spec.Template.Spec)
208+
}
202209

203210
if err := controllerutil.SetControllerReference(instance, desired, r.scheme); err != nil {
204211
return fmt.Errorf("setting owner reference: %w", err)
205212
}
206213

207-
logger := ctrl.LoggerFrom(ctx)
208214
nsName := types.NamespacedName{Name: desired.Name, Namespace: desired.Namespace}
209215
current := &appsv1.DaemonSet{}
210216
err := r.client.Get(ctx, nsName, current)

internal/controller/datadogcsidriver/controller_test.go

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package datadogcsidriver
88
import (
99
"context"
1010
"fmt"
11+
"reflect"
1112
"testing"
1213

1314
"github.com/stretchr/testify/assert"
@@ -28,14 +29,15 @@ import (
2829
"github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1"
2930
"github.com/DataDog/datadog-operator/pkg/images"
3031
"github.com/DataDog/datadog-operator/pkg/kubernetes"
32+
"github.com/DataDog/datadog-operator/pkg/untaint"
3133
)
3234

3335
const (
3436
testNamespace = "datadog"
3537
testName = "datadog-csi"
3638
)
3739

38-
func newTestReconciler(t *testing.T, objects ...client.Object) (*Reconciler, client.Client) {
40+
func newTestReconciler(t *testing.T, untaintControllerEnabled bool, objects ...client.Object) (*Reconciler, client.Client) {
3941
t.Helper()
4042
s := scheme.Scheme
4143
s.AddKnownTypes(v1alpha1.GroupVersion,
@@ -52,7 +54,7 @@ func newTestReconciler(t *testing.T, objects ...client.Object) (*Reconciler, cli
5254
// Set the default controller-runtime logger so ctrl.LoggerFrom(ctx) works in tests
5355
ctrl.SetLogger(zap.New(zap.UseDevMode(true)))
5456
recorder := record.NewFakeRecorder(10)
55-
r := NewReconciler(c, s, recorder)
57+
r := NewReconciler(c, s, recorder, untaintControllerEnabled)
5658

5759
return r, c
5860
}
@@ -73,7 +75,7 @@ func defaultCSIDriverCR() *v1alpha1.DatadogCSIDriver {
7375

7476
func TestReconcile_CreatesResources(t *testing.T) {
7577
instance := defaultCSIDriverCR()
76-
r, c := newTestReconciler(t, instance)
78+
r, c := newTestReconciler(t, false, instance)
7779
ctx := context.Background()
7880

7981
// First reconcile: adds finalizer
@@ -150,7 +152,7 @@ func TestReconcile_CustomSocketPaths(t *testing.T) {
150152
instance.Spec.APMSocketPath = &customAPM
151153
instance.Spec.DSDSocketPath = &customDSD
152154

153-
r, c := newTestReconciler(t, instance)
155+
r, c := newTestReconciler(t, false, instance)
154156
ctx := context.Background()
155157

156158
// Reconcile twice (finalizer + create)
@@ -181,7 +183,7 @@ func TestReconcile_CustomSocketPaths(t *testing.T) {
181183

182184
func TestReconcile_Deletion(t *testing.T) {
183185
instance := defaultCSIDriverCR()
184-
r, c := newTestReconciler(t, instance)
186+
r, c := newTestReconciler(t, false, instance)
185187
ctx := context.Background()
186188

187189
// Reconcile to add finalizer + create resources
@@ -224,7 +226,7 @@ func TestReconcile_Deletion(t *testing.T) {
224226

225227
func TestReconcile_UpdateDaemonSetOnSpecChange(t *testing.T) {
226228
instance := defaultCSIDriverCR()
227-
r, c := newTestReconciler(t, instance)
229+
r, c := newTestReconciler(t, false, instance)
228230
ctx := context.Background()
229231

230232
// Reconcile to create resources
@@ -266,7 +268,7 @@ func TestReconcile_UpdateDaemonSetOnSpecChange(t *testing.T) {
266268

267269
func TestReconcile_IdempotentNoUpdate(t *testing.T) {
268270
instance := defaultCSIDriverCR()
269-
r, c := newTestReconciler(t, instance)
271+
r, c := newTestReconciler(t, false, instance)
270272
ctx := context.Background()
271273

272274
// Reconcile to create resources
@@ -316,7 +318,7 @@ func TestReconcile_CSIDriverLabelsAdoption(t *testing.T) {
316318
},
317319
}
318320

319-
r, c := newTestReconciler(t, instance, existingCSIDriver)
321+
r, c := newTestReconciler(t, false, instance, existingCSIDriver)
320322
ctx := context.Background()
321323

322324
// Reconcile: add finalizer
@@ -369,7 +371,7 @@ func TestReconcile_Overrides(t *testing.T) {
369371
},
370372
}
371373

372-
r, c := newTestReconciler(t, instance)
374+
r, c := newTestReconciler(t, false, instance)
373375
ctx := context.Background()
374376

375377
// Reconcile twice (finalizer + create)
@@ -388,7 +390,7 @@ func TestReconcile_Overrides(t *testing.T) {
388390
// Labels merged into pod template
389391
assert.Equal(t, "containers", ds.Spec.Template.Labels["team"])
390392
// Default labels still present
391-
assert.Equal(t, csiDsName, ds.Spec.Template.Labels[appLabelKey])
393+
assert.Equal(t, csiDsName, ds.Spec.Template.Labels[AppLabelKey])
392394

393395
// Tolerations applied
394396
require.Len(t, ds.Spec.Template.Spec.Tolerations, 1)
@@ -423,7 +425,7 @@ func TestReconcile_StatusConditionOnCSIDriverError(t *testing.T) {
423425
// Instead, we test that when the reconcile succeeds, the condition is Ready=True,
424426
// and verify the status structure.
425427
instance := defaultCSIDriverCR()
426-
r, c := newTestReconciler(t, instance)
428+
r, c := newTestReconciler(t, false, instance)
427429
ctx := context.Background()
428430

429431
// Reconcile to add finalizer
@@ -450,7 +452,7 @@ func TestReconcile_StatusConditionOnCSIDriverError(t *testing.T) {
450452

451453
func TestReconcile_CSIDriverSpecDriftIsReconciled(t *testing.T) {
452454
instance := defaultCSIDriverCR()
453-
r, c := newTestReconciler(t, instance)
455+
r, c := newTestReconciler(t, false, instance)
454456
ctx := context.Background()
455457

456458
// Reconcile to create resources.
@@ -487,6 +489,49 @@ func TestReconcile_CSIDriverSpecDriftIsReconciled(t *testing.T) {
487489
assert.Contains(t, csiDriver.Spec.VolumeLifecycleModes, storagev1.VolumeLifecycleEphemeral)
488490
}
489491

492+
func TestReconcile_DaemonSetIncludesStartupTolerationWhenUntaintEnabled(t *testing.T) {
493+
instance := defaultCSIDriverCR()
494+
r, c := newTestReconciler(t, true, instance)
495+
ctx := context.Background()
496+
497+
_, err := r.Reconcile(ctx, instance)
498+
require.NoError(t, err)
499+
require.NoError(t, c.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, instance))
500+
501+
_, err = r.Reconcile(ctx, instance)
502+
require.NoError(t, err)
503+
504+
ds := &appsv1.DaemonSet{}
505+
require.NoError(t, c.Get(ctx, types.NamespacedName{Name: csiDsName, Namespace: testNamespace}, ds))
506+
want := untaint.AgentNotReadyEqualToleration()
507+
assert.True(t, tolerationListContains(ds.Spec.Template.Spec.Tolerations, want),
508+
"expected %+v in %+v", want, ds.Spec.Template.Spec.Tolerations)
509+
}
510+
511+
func TestReconcile_DaemonSetOmitsStartupTolerationWhenUntaintDisabled(t *testing.T) {
512+
instance := defaultCSIDriverCR()
513+
r, c := newTestReconciler(t, false, instance)
514+
ctx := context.Background()
515+
_, err := r.Reconcile(ctx, instance)
516+
require.NoError(t, err)
517+
require.NoError(t, c.Get(ctx, types.NamespacedName{Name: testName, Namespace: testNamespace}, instance))
518+
_, err = r.Reconcile(ctx, instance)
519+
require.NoError(t, err)
520+
ds := &appsv1.DaemonSet{}
521+
require.NoError(t, c.Get(ctx, types.NamespacedName{Name: csiDsName, Namespace: testNamespace}, ds))
522+
want := untaint.AgentNotReadyEqualToleration()
523+
assert.False(t, tolerationListContains(ds.Spec.Template.Spec.Tolerations, want))
524+
}
525+
526+
func tolerationListContains(tols []corev1.Toleration, want corev1.Toleration) bool {
527+
for i := range tols {
528+
if reflect.DeepEqual(tols[i], want) {
529+
return true
530+
}
531+
}
532+
return false
533+
}
534+
490535
// findCondition returns the condition with the given type, or nil.
491536
func findCondition(conditions []metav1.Condition, condType string) *metav1.Condition {
492537
for i := range conditions {

internal/controller/datadogcsidriver/daemonset.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ func buildDaemonSet(instance *datadoghqv1alpha1.DatadogCSIDriver) *appsv1.Daemon
2929
dsdSocketDir := filepath.Dir(dsdSocketPath)
3030

3131
labels := map[string]string{
32-
appLabelKey: csiDsName,
32+
AppLabelKey: csiDsName,
3333
}
3434
podLabels := map[string]string{
35-
appLabelKey: csiDsName,
35+
AppLabelKey: csiDsName,
3636
admissionControllerEnabledLabel: "false",
3737
}
3838

@@ -50,7 +50,7 @@ func buildDaemonSet(instance *datadoghqv1alpha1.DatadogCSIDriver) *appsv1.Daemon
5050
Spec: appsv1.DaemonSetSpec{
5151
Selector: &metav1.LabelSelector{
5252
MatchLabels: map[string]string{
53-
appLabelKey: csiDsName,
53+
AppLabelKey: csiDsName,
5454
},
5555
},
5656
RevisionHistoryLimit: &revisionHistoryLimit,

internal/controller/datadogcsidriver_controller.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ import (
2727

2828
// DatadogCSIDriverReconciler reconciles a DatadogCSIDriver object.
2929
type DatadogCSIDriverReconciler struct {
30-
Client client.Client
31-
Scheme *runtime.Scheme
32-
Recorder record.EventRecorder
33-
internal *datadogcsidriver.Reconciler
30+
Client client.Client
31+
Scheme *runtime.Scheme
32+
Recorder record.EventRecorder
33+
UntaintControllerEnabled bool
34+
internal *datadogcsidriver.Reconciler
3435
}
3536

3637
// RBACs for DatadogCSIDriver objects
@@ -60,7 +61,7 @@ func (r *DatadogCSIDriverReconciler) Reconcile(ctx context.Context, instance *da
6061

6162
// SetupWithManager creates a new DatadogCSIDriver controller.
6263
func (r *DatadogCSIDriverReconciler) SetupWithManager(mgr ctrl.Manager) error {
63-
r.internal = datadogcsidriver.NewReconciler(r.Client, r.Scheme, r.Recorder)
64+
r.internal = datadogcsidriver.NewReconciler(r.Client, r.Scheme, r.Recorder, r.UntaintControllerEnabled)
6465

6566
or := reconcile.AsReconciler[*datadoghqv1alpha1.DatadogCSIDriver](r.Client, r)
6667
return ctrl.NewControllerManagedBy(mgr).

0 commit comments

Comments
 (0)