Skip to content

Commit 9dbc84c

Browse files
committed
fix(fluentd): add opt-in configCheckPod overrides instead of propagating sidecars
Revert the SidecarContainers-append to the configcheck pod's Containers list: it's a run-to-completion pod gated on PodSucceeded, so a long-running sidecar (e.g. the sleep-infinity example in our own docs) wedges it forever and blocks Fluentd StatefulSet reconciliation, and its volume set diverges from the StatefulSet's so a volume-mounting sidecar makes the pod uncreatable. Instead, add FluentdSpec.ConfigCheckPod, a narrow opt-in override (initContainers, volumes, activeDeadlineSeconds) merged onto the generated check pod last, mirroring the pattern already used by SyslogNGSpec.ConfigCheckPodOverrides. RestartPolicy is reasserted to Never after the merge, and a PodFailed check pod with Reason=DeadlineExceeded is now treated as not-ready and deleted for retry instead of being reported as an invalid config. Report: kube-logging#2103 PR: kube-logging#2303 Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com>
1 parent 005f85c commit 9dbc84c

11 files changed

Lines changed: 9356 additions & 22 deletions

File tree

charts/logging-operator/charts/logging-operator-crds/templates/logging.banzaicloud.io_fluentdconfigs.yaml

Lines changed: 1532 additions & 0 deletions
Large diffs are not rendered by default.

charts/logging-operator/charts/logging-operator-crds/templates/logging.banzaicloud.io_loggings.yaml

Lines changed: 1532 additions & 0 deletions
Large diffs are not rendered by default.

charts/logging-operator/crds/logging.banzaicloud.io_fluentdconfigs.yaml

Lines changed: 1532 additions & 0 deletions
Large diffs are not rendered by default.

charts/logging-operator/crds/logging.banzaicloud.io_loggings.yaml

Lines changed: 1532 additions & 0 deletions
Large diffs are not rendered by default.

config/crd/bases/logging.banzaicloud.io_fluentdconfigs.yaml

Lines changed: 1532 additions & 0 deletions
Large diffs are not rendered by default.

config/crd/bases/logging.banzaicloud.io_loggings.yaml

Lines changed: 1532 additions & 0 deletions
Large diffs are not rendered by default.

docs/configuration/crds/v1beta1/fluentd_types.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ Overrides the default logging level configCheck setup. This field is not used di
4545
### configCheckAnnotations (map[string]string, optional) {#fluentdspec-configcheckannotations}
4646

4747

48+
### configCheckPod (*ConfigCheckPodOverrides, optional) {#fluentdspec-configcheckpod}
49+
50+
ConfigCheckPod lets you add helper containers and volumes to the transient configcheck pod, which runs `fluentd --dry-run` once and must exit on its own. Long-running helpers must be declared as native sidecars (initContainers with restartPolicy: Always, k8s 1.29+), otherwise the pod never completes and config rollout stops. This is a narrower counterpart to SyslogNGSpec.ConfigCheckPodOverrides: the Fluentd check pod already inherits nodeSelector/tolerations/affinity/priorityClassName/securityContext/ imagePullSecrets/dnsPolicy/dnsConfig/serviceAccount from FluentdSpec, so only extra containers, check-pod-only volumes and a deadline are exposed here. Note: the check pod is named after a hash of the rendered config and is only ever created, never updated, so changing configCheckPod alone does not re-run the check against the current pod - it takes effect on the next config change, or after manually deleting the existing check pod.
51+
52+
4853
### configCheckResources (corev1.ResourceRequirements, optional) {#fluentdspec-configcheckresources}
4954

5055

@@ -202,6 +207,27 @@ Duration in seconds for graceful pod termination. Set higher than expected clean
202207

203208

204209

210+
## ConfigCheckPodOverrides
211+
212+
ConfigCheckPodOverrides adds helper containers, volumes and a deadline to the
213+
transient fluentd configcheck pod, merged onto the generated pod spec last.
214+
215+
### activeDeadlineSeconds (*int64, optional) {#configcheckpodoverrides-activedeadlineseconds}
216+
217+
ActiveDeadlineSeconds bounds how long the configcheck pod may run before it is treated as failed and deleted so a new one can be created and retried. Without it, a helper container that never terminates leaves the pod running indefinitely and blocks config rollout, with no timeout.
218+
219+
220+
### initContainers ([]corev1.Container, optional) {#configcheckpodoverrides-initcontainers}
221+
222+
InitContainers to add to the configcheck pod. A plain init container is only guaranteed to have started, not to have finished, before the dry-run container starts, so it does not order a preparation step against the check - it can race it. A long-running helper must instead set restartPolicy: Always (a native sidecar, k8s 1.29+); the kubelet then terminates it once the dry-run container exits, letting the pod reach Succeeded/Failed.
223+
224+
225+
### volumes ([]corev1.Volume, optional) {#configcheckpodoverrides-volumes}
226+
227+
Volumes available to the configcheck pod only. This is the check-pod counterpart to FluentdSpec.ExtraVolumes, which is not mounted on the check pod, e.g. for a volume an InitContainers entry above needs.
228+
229+
230+
205231
## FluentOutLogrotate
206232

207233
### age (string, optional) {#fluentoutlogrotate-age}

pkg/resources/fluentd/appconfigmap.go

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"maps"
2222

2323
"emperror.dev/errors"
24+
"github.com/cisco-open/operator-tools/pkg/merge"
2425
"github.com/cisco-open/operator-tools/pkg/reconciler"
2526
"github.com/cisco-open/operator-tools/pkg/utils"
2627
"github.com/spf13/cast"
@@ -70,8 +71,10 @@ func (r *Reconciler) configHash() (string, error) {
7071
}
7172

7273
func (r *Reconciler) hasConfigCheckPod(ctx context.Context, hashKey string, fluentdSpec v1beta1.FluentdSpec) (bool, error) {
73-
var err error
74-
pod := r.newCheckPod(hashKey, fluentdSpec)
74+
pod, err := r.newCheckPod(hashKey, fluentdSpec)
75+
if err != nil {
76+
return false, err
77+
}
7578

7679
p := &corev1.Pod{}
7780
err = r.Client.Get(ctx, client.ObjectKeyFromObject(pod), p)
@@ -143,7 +146,10 @@ func (r *Reconciler) configCheck(ctx context.Context) (*ConfigCheckResult, error
143146
return res, errors.WrapIf(err, "failed to find output secret for fluentd configcheck")
144147
}
145148

146-
pod := r.newCheckPod(hashKey, *r.fluentdSpec)
149+
pod, err := r.newCheckPod(hashKey, *r.fluentdSpec)
150+
if err != nil {
151+
return nil, err
152+
}
147153
configcheck.WithHashLabel(pod, hashKey)
148154

149155
existingPods := &corev1.PodList{}
@@ -184,6 +190,17 @@ func (r *Reconciler) configCheck(ctx context.Context) (*ConfigCheckResult, error
184190
case corev1.PodRunning:
185191
return &ConfigCheckResult{}, nil
186192
case corev1.PodFailed:
193+
if pod.Status.Reason == "DeadlineExceeded" {
194+
// the check ran past configCheckPod.activeDeadlineSeconds, not a config
195+
// validation failure; delete the pod so a fresh one is created and retried
196+
if err := client.IgnoreNotFound(r.Client.Delete(ctx, pod)); err != nil {
197+
return nil, errors.WrapIf(err, "failed to delete configcheck pod that exceeded its active deadline")
198+
}
199+
return &ConfigCheckResult{
200+
Ready: false,
201+
Message: "configcheck pod exceeded its active deadline and was deleted, will retry",
202+
}, nil
203+
}
187204
return &ConfigCheckResult{
188205
Ready: true,
189206
Valid: false,
@@ -262,7 +279,7 @@ func (r *Reconciler) newCheckOutputSecret(hashKey string) (*corev1.Secret, error
262279
return nil, errors.New("output secret is invalid, unable to create output secret for config check")
263280
}
264281

265-
func (r *Reconciler) newCheckPod(hashKey string, fluentdSpec v1beta1.FluentdSpec) *corev1.Pod {
282+
func (r *Reconciler) newCheckPod(hashKey string, fluentdSpec v1beta1.FluentdSpec) (*corev1.Pod, error) {
266283
volumes := r.volumesCheckPod(hashKey, fluentdSpec)
267284
container := r.containerCheckPod(fluentdSpec)
268285
initContainer := r.initContainerCheckPod(fluentdSpec)
@@ -314,7 +331,15 @@ func (r *Reconciler) newCheckPod(hashKey string, fluentdSpec v1beta1.FluentdSpec
314331
}
315332
}
316333

317-
return pod
334+
if fluentdSpec.ConfigCheckPod != nil {
335+
if err := merge.Merge(&pod.Spec, fluentdSpec.ConfigCheckPod); err != nil {
336+
return nil, errors.WrapIf(err, "failed to merge configCheckPod overrides into the configcheck pod")
337+
}
338+
// the merge must not be able to turn the check pod into a long-lived one
339+
pod.Spec.RestartPolicy = corev1.RestartPolicyNever
340+
}
341+
342+
return pod, nil
318343
}
319344

320345
func (r *Reconciler) volumesCheckPod(hashKey string, fluentdSpec v1beta1.FluentdSpec) (v []corev1.Volume) {
@@ -430,10 +455,6 @@ func (r *Reconciler) containerCheckPod(fluentdSpec v1beta1.FluentdSpec) []corev1
430455
},
431456
}
432457

433-
if len(fluentdSpec.SidecarContainers) != 0 {
434-
container = append(container, fluentdSpec.SidecarContainers...)
435-
}
436-
437458
return container
438459
}
439460

pkg/resources/fluentd/appconfigmap_test.go

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ func TestNewCheckPodDNSSettings(t *testing.T) {
9090
}
9191
r := newCheckPodReconciler(t, spec)
9292

93-
pod := r.newCheckPod("deadbeef", *r.fluentdSpec)
93+
pod, err := r.newCheckPod("deadbeef", *r.fluentdSpec)
94+
require.NoError(t, err)
9495

9596
assert.Equal(t, tt.expectedDNSPolicy, pod.Spec.DNSPolicy)
9697
assert.Equal(t, tt.dnsConfig, pod.Spec.DNSConfig)
@@ -112,7 +113,8 @@ func TestNewCheckPodDNSSettingsMatchStatefulSet(t *testing.T) {
112113
}
113114
r := newCheckPodReconciler(t, spec)
114115

115-
checkPod := r.newCheckPod("deadbeef", *r.fluentdSpec)
116+
checkPod, err := r.newCheckPod("deadbeef", *r.fluentdSpec)
117+
require.NoError(t, err)
116118

117119
obj, _, err := r.statefulset()
118120
require.NoError(t, err)
@@ -123,22 +125,38 @@ func TestNewCheckPodDNSSettingsMatchStatefulSet(t *testing.T) {
123125
assert.Equal(t, sts.Spec.Template.Spec.DNSConfig, checkPod.Spec.DNSConfig)
124126
}
125127

126-
// TestNewCheckPodSidecarContainers pins the configcheck pod to carry the same
127-
// sidecarContainers as the aggregator StatefulSet, since a sidecar that only
128-
// mutates shared config (e.g. refreshing a GeoIP database via extraVolumes)
129-
// needs to run before the check as well, or the check validates against stale
130-
// input.
131-
func TestNewCheckPodSidecarContainers(t *testing.T) {
128+
// TestNewCheckPodConfigCheckPodOverrides pins configCheckPod.{initContainers,
129+
// volumes,activeDeadlineSeconds} to land on the generated configcheck pod, and
130+
// the pod to stay run-to-completion (RestartPolicy: Never) even though a merged
131+
// init container may declare RestartPolicy: Always (a native sidecar).
132+
func TestNewCheckPodConfigCheckPodOverrides(t *testing.T) {
133+
restartAlways := corev1.ContainerRestartPolicyAlways
132134
sidecar := corev1.Container{
133-
Name: "fluentd-sidecar",
134-
Image: "busybox:1.37",
135+
Name: "geoip-refresh",
136+
Image: "busybox:1.37",
137+
RestartPolicy: &restartAlways,
138+
}
139+
extraVolume := corev1.Volume{
140+
Name: "geoip-db",
141+
VolumeSource: corev1.VolumeSource{
142+
EmptyDir: &corev1.EmptyDirVolumeSource{},
143+
},
135144
}
145+
deadline := int64(120)
136146
spec := &v1beta1.FluentdSpec{
137-
SidecarContainers: []corev1.Container{sidecar},
147+
ConfigCheckPod: &v1beta1.ConfigCheckPodOverrides{
148+
InitContainers: []corev1.Container{sidecar},
149+
Volumes: []corev1.Volume{extraVolume},
150+
ActiveDeadlineSeconds: &deadline,
151+
},
138152
}
139153
r := newCheckPodReconciler(t, spec)
140154

141-
checkPod := r.newCheckPod("deadbeef", *r.fluentdSpec)
155+
checkPod, err := r.newCheckPod("deadbeef", *r.fluentdSpec)
156+
require.NoError(t, err)
142157

143-
assert.Contains(t, checkPod.Spec.Containers, sidecar)
158+
assert.Contains(t, checkPod.Spec.InitContainers, sidecar)
159+
assert.Contains(t, checkPod.Spec.Volumes, extraVolume)
160+
assert.Equal(t, &deadline, checkPod.Spec.ActiveDeadlineSeconds)
161+
assert.Equal(t, corev1.RestartPolicyNever, checkPod.Spec.RestartPolicy)
144162
}

pkg/sdk/logging/api/v1beta1/fluentd_types.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,44 @@ type FluentdSpec struct {
122122
// Overrides the default logging level configCheck setup.
123123
// This field is not used directly, its fields are copied over the ones in the logging resource, field by field, so setting one field here does not discard the others set on the logging resource.
124124
ConfigCheck *ConfigCheck `json:"configCheck,omitempty"`
125+
// ConfigCheckPod lets you add helper containers and volumes to the transient
126+
// configcheck pod, which runs `fluentd --dry-run` once and must exit on its
127+
// own. Long-running helpers must be declared as native sidecars
128+
// (initContainers with restartPolicy: Always, k8s 1.29+), otherwise the pod
129+
// never completes and config rollout stops. This is a narrower counterpart
130+
// to SyslogNGSpec.ConfigCheckPodOverrides: the Fluentd check pod already
131+
// inherits nodeSelector/tolerations/affinity/priorityClassName/securityContext/
132+
// imagePullSecrets/dnsPolicy/dnsConfig/serviceAccount from FluentdSpec, so only
133+
// extra containers, check-pod-only volumes and a deadline are exposed here.
134+
// Note: the check pod is named after a hash of the rendered config and is
135+
// only ever created, never updated, so changing configCheckPod alone does
136+
// not re-run the check against the current pod - it takes effect on the
137+
// next config change, or after manually deleting the existing check pod.
138+
ConfigCheckPod *ConfigCheckPodOverrides `json:"configCheckPod,omitempty"`
139+
}
140+
141+
// +kubebuilder:object:generate=true
142+
143+
// ConfigCheckPodOverrides adds helper containers, volumes and a deadline to the
144+
// transient fluentd configcheck pod, merged onto the generated pod spec last.
145+
type ConfigCheckPodOverrides struct {
146+
// InitContainers to add to the configcheck pod. A plain init container is
147+
// only guaranteed to have started, not to have finished, before the dry-run
148+
// container starts, so it does not order a preparation step against the
149+
// check - it can race it. A long-running helper must instead set
150+
// restartPolicy: Always (a native sidecar, k8s 1.29+); the kubelet then
151+
// terminates it once the dry-run container exits, letting the pod reach
152+
// Succeeded/Failed.
153+
InitContainers []corev1.Container `json:"initContainers,omitempty"`
154+
// Volumes available to the configcheck pod only. This is the check-pod
155+
// counterpart to FluentdSpec.ExtraVolumes, which is not mounted on the
156+
// check pod, e.g. for a volume an InitContainers entry above needs.
157+
Volumes []corev1.Volume `json:"volumes,omitempty"`
158+
// ActiveDeadlineSeconds bounds how long the configcheck pod may run before
159+
// it is treated as failed and deleted so a new one can be created and
160+
// retried. Without it, a helper container that never terminates leaves the
161+
// pod running indefinitely and blocks config rollout, with no timeout.
162+
ActiveDeadlineSeconds *int64 `json:"activeDeadlineSeconds,omitempty"`
125163
}
126164

127165
// +kubebuilder:object:generate=true

0 commit comments

Comments
 (0)