From 8713b5f86960e255a77807f0a6e43e6af214532d Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Fri, 2 Oct 2026 23:19:18 +0200 Subject: [PATCH 01/28] fix: explicitly propagate DD_DISCOVERY_ENABLED=false when service discovery is disabled Configure() previously left the Agent component unrequired when service discovery resolved to disabled, so ManageNodeAgent never ran and the env var was simply omitted instead of written as "false". The agent defaults service discovery to on for versions >= 7.78.0, so an explicit false was silently overridden by that default. Co-Authored-By: Claude Sonnet 5 --- .../feature/servicediscovery/feature.go | 24 +++++++++++++++-- .../feature/servicediscovery/feature_test.go | 26 ++++++++++++++++++- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/internal/controller/datadogagent/feature/servicediscovery/feature.go b/internal/controller/datadogagent/feature/servicediscovery/feature.go index 6ef5f9c521..14ee46672b 100644 --- a/internal/controller/datadogagent/feature/servicediscovery/feature.go +++ b/internal/controller/datadogagent/feature/servicediscovery/feature.go @@ -33,6 +33,7 @@ func buildFeature(*feature.Options) feature.Feature { } type serviceDiscoveryFeature struct { + enabled bool useSystemProbeLite bool } @@ -43,13 +44,22 @@ func (f *serviceDiscoveryFeature) ID() feature.IDType { // Configure is used to configure the feature from a v2alpha1.DatadogAgent instance. func (f *serviceDiscoveryFeature) Configure(_ metav1.Object, ddaSpec *v2alpha1.DatadogAgentSpec, _ *v2alpha1.RemoteConfigConfiguration) (reqComp feature.RequiredComponents) { + f.enabled = resolveEnabled(ddaSpec) f.useSystemProbeLite = false - if resolveEnabled(ddaSpec) { + + if f.enabled { f.useSystemProbeLite = shouldEnableServiceDiscoveryByDefault(ddaSpec) reqComp.Agent = feature.RequiredComponent{ IsRequired: new(true), Containers: []apicommon.AgentContainerName{apicommon.CoreAgentContainerName, apicommon.SystemProbeContainerName}, } + } else { + // Not required, but still needs the core agent container configured so that + // DD_DISCOVERY_ENABLED is explicitly written as "false" instead of being omitted + // (the agent defaults service discovery to on for versions >= 7.78.0). + reqComp.Agent = feature.RequiredComponent{ + Containers: []apicommon.AgentContainerName{apicommon.CoreAgentContainerName}, + } } return reqComp @@ -98,6 +108,16 @@ func (f *serviceDiscoveryFeature) ManageClusterAgent(managers feature.PodTemplat // ManageNodeAgent allows a feature to configure the Node Agent's corev1.PodTemplateSpec // It should do nothing if the feature doesn't need to configure it. func (f *serviceDiscoveryFeature) ManageNodeAgent(managers feature.PodTemplateManagers) error { + // Add the env var to explicitly reflect the resolved enabled state. + // Otherwise, this feature defaults to enabled in the Agent code for versions >= 7.78.0. + managers.EnvVar().AddEnvVarToContainer(apicommon.CoreAgentContainerName, &corev1.EnvVar{ + Name: common.DDServiceDiscoveryEnabled, + Value: apiutils.BoolToString(&f.enabled), + }) + if !f.enabled { + return nil + } + // annotations managers.Annotation().AddAnnotation(common.SystemProbeAppArmorAnnotationKey, common.SystemProbeAppArmorAnnotationValue) @@ -127,7 +147,7 @@ func (f *serviceDiscoveryFeature) ManageNodeAgent(managers feature.PodTemplateMa Value: "true", } - managers.EnvVar().AddEnvVarToContainers([]apicommon.AgentContainerName{apicommon.CoreAgentContainerName, apicommon.SystemProbeContainerName}, enableEnvVar) + managers.EnvVar().AddEnvVarToContainer(apicommon.SystemProbeContainerName, enableEnvVar) managers.EnvVar().AddEnvVarToInitContainer(apicommon.InitConfigContainerName, enableEnvVar) if f.useSystemProbeLite { diff --git a/internal/controller/datadogagent/feature/servicediscovery/feature_test.go b/internal/controller/datadogagent/feature/servicediscovery/feature_test.go index 9b49a5a624..0285f31e25 100644 --- a/internal/controller/datadogagent/feature/servicediscovery/feature_test.go +++ b/internal/controller/datadogagent/feature/servicediscovery/feature_test.go @@ -50,7 +50,8 @@ func Test_serviceDiscoveryFeature_Configure(t *testing.T) { { Name: "service discovery not enabled", DDA: ddaServiceDiscoveryDisabled.DeepCopy(), - WantConfigure: false, + WantConfigure: true, + Agent: test.NewDefaultComponentTest().WithWantFunc(getDisabledWantFunc()), }, { Name: "service discovery enabled", @@ -255,6 +256,29 @@ func serviceDiscoveryEnabledForVersion(version string) bool { return pkgutils.IsAboveMinVersion(version, serviceDiscoveryAutoEnableMinVersion, nil) } +// getDisabledWantFunc asserts that, when service discovery is explicitly disabled, the core +// agent container still receives an explicit DD_DISCOVERY_ENABLED="false" env var instead of +// omitting it entirely (which would let the agent's own >= 7.78.0 auto-on default win), and +// that none of the system-probe-specific configuration is applied. +func getDisabledWantFunc() func(t testing.TB, mgrInterface feature.PodTemplateManagers) { + return func(t testing.TB, mgrInterface feature.PodTemplateManagers) { + mgr := mgrInterface.(*fake.PodTemplateManagers) + + wantAgentEnvVars := []*corev1.EnvVar{ + { + Name: common.DDServiceDiscoveryEnabled, + Value: "false", + }, + } + agentEnvVars := mgr.EnvVarMgr.EnvVarsByC[apicommon.CoreAgentContainerName] + assert.True(t, apiutils.IsEqualStruct(agentEnvVars, wantAgentEnvVars), "Agent env vars \ndiff = %s", cmp.Diff(agentEnvVars, wantAgentEnvVars)) + + assert.Empty(t, mgr.EnvVarMgr.EnvVarsByC[apicommon.SystemProbeContainerName]) + assert.Empty(t, mgr.VolumeMountMgr.VolumeMountsByC[apicommon.SystemProbeContainerName]) + assert.Empty(t, mgr.VolumeMgr.Volumes) + } +} + func getWantFunc(wantSystemProbeLite bool) func(t testing.TB, mgrInterface feature.PodTemplateManagers) { return func(t testing.TB, mgrInterface feature.PodTemplateManagers) { mgr := mgrInterface.(*fake.PodTemplateManagers) From 012a73c649ec54f859cb16d67a35dbc0377bd8e8 Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Fri, 2 Oct 2026 23:19:25 +0200 Subject: [PATCH 02/28] fix: don't enable instrumentationCrd when the cluster agent is disabled The instrumentation-checks autodiscovery provider requires a live Cluster Agent to serve configs to the node agent. Configure() only checked agent/cluster-agent version floors, never whether the Cluster Agent was actually going to be deployed (override.clusterAgent.disabled). Without one, the node agent kept registering the provider and spamming connection errors trying to reach a Cluster Agent Service that doesn't exist. Co-Authored-By: Claude Sonnet 5 --- .../feature/instrumentationcrd/feature.go | 8 ++++++++ .../feature/instrumentationcrd/feature_test.go | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/internal/controller/datadogagent/feature/instrumentationcrd/feature.go b/internal/controller/datadogagent/feature/instrumentationcrd/feature.go index 3ef3f7be90..6ed9eb1a4e 100644 --- a/internal/controller/datadogagent/feature/instrumentationcrd/feature.go +++ b/internal/controller/datadogagent/feature/instrumentationcrd/feature.go @@ -12,6 +12,7 @@ import ( apicommon "github.com/DataDog/datadog-operator/api/datadoghq/common" "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" + apiutils "github.com/DataDog/datadog-operator/api/utils" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/common" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature" featureutils "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/utils" @@ -61,6 +62,13 @@ func (f *instrumentationCRDFeature) Configure(dda metav1.Object, ddaSpec *v2alph return feature.RequiredComponents{} } + // The instrumentation-checks autodiscovery provider requires a live Cluster Agent to serve + // configs to the node agent; without one the node agent just spams connection errors trying + // to reach a Cluster Agent Kubernetes Service that doesn't exist. + if clusterAgent, ok := ddaSpec.Override[v2alpha1.ClusterAgentComponentName]; ok && apiutils.BoolValue(clusterAgent.Disabled) { + return feature.RequiredComponents{} + } + // If the cluster agent version is explicitly set and below the minimum, skip enabling. if clusterAgent, ok := ddaSpec.Override[v2alpha1.ClusterAgentComponentName]; ok && clusterAgent.Image != nil { version := common.GetAgentVersionFromImage(*clusterAgent.Image) diff --git a/internal/controller/datadogagent/feature/instrumentationcrd/feature_test.go b/internal/controller/datadogagent/feature/instrumentationcrd/feature_test.go index 7264a7c546..31365458b3 100644 --- a/internal/controller/datadogagent/feature/instrumentationcrd/feature_test.go +++ b/internal/controller/datadogagent/feature/instrumentationcrd/feature_test.go @@ -97,6 +97,17 @@ func Test_instrumentationCRDFeature_Configure(t *testing.T) { ClusterAgent: instrumentationCRDClusterAgentFunc(false), Agent: instrumentationCRDAgentFunc(false), }, + { + Name: "InstrumentationCRD disabled when cluster agent is disabled, even if versions meet minimum", + DDA: testutils.NewInitializedDatadogAgentBuilder(resourcesNamespace, resourcesName). + WithClusterAgentImage("cluster-agent:7.82.0"). + WithNodeAgentImage("agent:7.82.0"). + WithClusterAgentDisabled(true). + Build(), + WantConfigure: false, + ClusterAgent: instrumentationCRDClusterAgentFunc(false), + Agent: instrumentationCRDAgentFunc(false), + }, } tests.Run(t, buildInstrumentationCRDFeature) From 6f32dcebca242a0444a80afb7ed4dd9a64d97145 Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Fri, 2 Oct 2026 23:19:31 +0200 Subject: [PATCH 03/28] fix: don't enable kubeStateMetricsCore when the cluster agent is disabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kubeStateMetricsCore only ever runs inside the Cluster Agent or Cluster Check Runners it dispatches to — there's no code path for it to run without one. Configure() gated purely on the feature's own enabled flag, never on override.clusterAgent.disabled, so disabling the cluster agent while the feature stayed enabled created a ClusterRole/ClusterRoleBinding bound to a nonexistent ServiceAccount plus a stray ConfigMap. Co-Authored-By: Claude Sonnet 5 --- .../datadogagent/feature/kubernetesstatecore/feature.go | 8 ++++++++ .../feature/kubernetesstatecore/feature_test.go | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/internal/controller/datadogagent/feature/kubernetesstatecore/feature.go b/internal/controller/datadogagent/feature/kubernetesstatecore/feature.go index 45d5b1dcac..e529890c85 100644 --- a/internal/controller/datadogagent/feature/kubernetesstatecore/feature.go +++ b/internal/controller/datadogagent/feature/kubernetesstatecore/feature.go @@ -94,6 +94,14 @@ func (f *ksmFeature) Configure(dda metav1.Object, ddaSpec *v2alpha1.DatadogAgent f.owner = dda var output feature.RequiredComponents + // This check only ever runs inside the Cluster Agent (or Cluster Check Runners it + // dispatches to) — without a live Cluster Agent there's no code path for it to run at + // all, and creating its RBAC/ConfigMap would just leave them bound to a nonexistent + // ServiceAccount. + if clusterAgent, ok := ddaSpec.Override[v2alpha1.ClusterAgentComponentName]; ok && apiutils.BoolValue(clusterAgent.Disabled) { + return output + } + if ddaSpec.Features != nil && ddaSpec.Features.KubeStateMetricsCore != nil && apiutils.BoolValue(ddaSpec.Features.KubeStateMetricsCore.Enabled) { output.ClusterAgent.IsRequired = new(true) output.ClusterAgent.Containers = []apicommon.AgentContainerName{apicommon.ClusterAgentContainerName} diff --git a/internal/controller/datadogagent/feature/kubernetesstatecore/feature_test.go b/internal/controller/datadogagent/feature/kubernetesstatecore/feature_test.go index 57c96920ed..d003481112 100644 --- a/internal/controller/datadogagent/feature/kubernetesstatecore/feature_test.go +++ b/internal/controller/datadogagent/feature/kubernetesstatecore/feature_test.go @@ -48,6 +48,14 @@ func Test_ksmFeature_Configure(t *testing.T) { Build(), WantConfigure: false, }, + { + Name: "ksm-core enabled but cluster agent disabled", + DDA: testutils.NewDatadogAgentBuilder(). + WithKSMEnabled(true). + WithClusterAgentDisabled(true). + Build(), + WantConfigure: false, + }, { Name: "ksm-core enabled", DDA: testutils.NewDatadogAgentBuilder(). From 645e5870f2022e8c2d67db4eb8b7d7a73e3d386d Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Sat, 3 Oct 2026 00:27:27 +0200 Subject: [PATCH 04/28] fix: don't create orchestratorExplorer RBAC when the cluster agent is disabled orchestratorExplorer runs inside the Cluster Agent (or Cluster Check Runners it dispatches to). Configure() never checked override.clusterAgent.disabled, so ManageDependencies still created a ClusterRole/ClusterRoleBinding bound to a nonexistent cluster-agent ServiceAccount. The node agent's explicit DD_ORCHESTRATOR_EXPLORER_ENABLED disable write is preserved. Co-Authored-By: Claude Sonnet 5 --- .../feature/orchestratorexplorer/feature.go | 20 +++++++++++++++++-- .../orchestratorexplorer/feature_test.go | 17 ++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/internal/controller/datadogagent/feature/orchestratorexplorer/feature.go b/internal/controller/datadogagent/feature/orchestratorexplorer/feature.go index b1dd8ede82..c1c9eb9435 100644 --- a/internal/controller/datadogagent/feature/orchestratorexplorer/feature.go +++ b/internal/controller/datadogagent/feature/orchestratorexplorer/feature.go @@ -51,6 +51,7 @@ func buildOrchestratorExplorerFeature(options *feature.Options) feature.Feature type orchestratorExplorerFeature struct { enabled bool + clusterAgentDisabled bool runInClusterChecksRunner bool scrubContainers bool collectKubernetesNetworkResources bool @@ -82,9 +83,18 @@ func (f *orchestratorExplorerFeature) Configure(dda metav1.Object, ddaSpec *v2al // Merge configuration from Status.RemoteConfigConfiguration into the Spec f.mergeConfigs(ddaSpec, ddaRCStatus) + // This check only ever runs inside the Cluster Agent (or Cluster Check Runners it + // dispatches to) — without a live Cluster Agent there's no code path for it to run at + // all, so treat it as disabled and skip requiring/configuring the Cluster Agent side + // (ManageDependencies also no-ops below) to avoid creating RBAC bound to a nonexistent + // ServiceAccount. The node agent still gets the explicit disable env var below. + if clusterAgent, ok := ddaSpec.Override[v2alpha1.ClusterAgentComponentName]; ok && apiutils.BoolValue(clusterAgent.Disabled) { + f.clusterAgentDisabled = true + } + orchestratorExplorer := ddaSpec.Features.OrchestratorExplorer - if orchestratorExplorer != nil && apiutils.BoolValue(orchestratorExplorer.Enabled) { + if !f.clusterAgentDisabled && orchestratorExplorer != nil && apiutils.BoolValue(orchestratorExplorer.Enabled) { f.enabled = true reqComp.ClusterAgent.IsRequired = new(true) reqComp.Agent.IsRequired = new(true) @@ -133,7 +143,9 @@ func (f *orchestratorExplorerFeature) Configure(dda metav1.Object, ddaSpec *v2al } } - reqComp.ClusterAgent.Containers = []apicommon.AgentContainerName{apicommon.ClusterAgentContainerName} + if !f.clusterAgentDisabled { + reqComp.ClusterAgent.Containers = []apicommon.AgentContainerName{apicommon.ClusterAgentContainerName} + } reqContainers := []apicommon.AgentContainerName{apicommon.CoreAgentContainerName} // Process Agent is not required as of agent version 7.51.0 if nodeAgent, ok := ddaSpec.Override[v2alpha1.NodeAgentComponentName]; ok { @@ -173,6 +185,10 @@ func (f *orchestratorExplorerFeature) mergeConfigs(ddaSpec *v2alpha1.DatadogAgen // ManageDependencies allows a feature to manage its dependencies. // Feature's dependencies should be added in the store. func (f *orchestratorExplorerFeature) ManageDependencies(managers feature.ResourceManagers) error { + if f.clusterAgentDisabled { + return nil + } + // Create a configMap if CustomConfig.ConfigData is provided and CustomConfig.ConfigMap == nil, // OR if the default configMap is needed. configCM, err := f.buildOrchestratorExplorerConfigMap() diff --git a/internal/controller/datadogagent/feature/orchestratorexplorer/feature_test.go b/internal/controller/datadogagent/feature/orchestratorexplorer/feature_test.go index 1c1dd79336..7082a854b8 100644 --- a/internal/controller/datadogagent/feature/orchestratorexplorer/feature_test.go +++ b/internal/controller/datadogagent/feature/orchestratorexplorer/feature_test.go @@ -59,6 +59,23 @@ func Test_orchestratorExplorerFeature_Configure(t *testing.T) { Build(), WantConfigure: true, }, + { + Name: "orchestrator explorer enabled but cluster agent disabled", + DDA: testutils.NewDatadogAgentBuilder(). + WithOrchestratorExplorerEnabled(true). + WithClusterAgentDisabled(true). + Build(), + WantConfigure: true, + WantDependenciesFunc: func(t testing.TB, sc store.StoreClient) { + _, found := sc.Get(kubernetes.ClusterRolesKind, "", "-orch-exp-dca") + assert.False(t, found, "should not create a ClusterRole bound to a nonexistent cluster agent ServiceAccount") + }, + Agent: test.NewDefaultComponentTest().WithWantFunc(func(t testing.TB, mgrInterface feature.PodTemplateManagers) { + mgr := mgrInterface.(*fake.PodTemplateManagers) + envVars := mgr.EnvVarMgr.EnvVarsByC[apicommon.CoreAgentContainerName] + assert.Contains(t, envVars, &corev1.EnvVar{Name: DDOrchestratorExplorerEnabled, Value: "false"}) + }), + }, { Name: "orchestrator explorer enabled", DDA: testutils.NewDatadogAgentBuilder(). From 629505634ea4973588435bd06c3f76d50e2172fc Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Sat, 3 Oct 2026 00:27:33 +0200 Subject: [PATCH 05/28] fix: don't enable eventCollection when the cluster agent is disabled Event collection is configured using the Cluster Agent only, and defaults to enabled. Configure() never checked override.clusterAgent.disabled, so ManageDependencies still created RBAC bound to a nonexistent cluster-agent ServiceAccount. Co-Authored-By: Claude Sonnet 5 --- .../datadogagent/feature/eventcollection/feature.go | 6 ++++++ .../datadogagent/feature/eventcollection/feature_test.go | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/internal/controller/datadogagent/feature/eventcollection/feature.go b/internal/controller/datadogagent/feature/eventcollection/feature.go index 45a3b1bd5c..1636b452f5 100644 --- a/internal/controller/datadogagent/feature/eventcollection/feature.go +++ b/internal/controller/datadogagent/feature/eventcollection/feature.go @@ -62,6 +62,12 @@ func (f *eventCollectionFeature) ID() feature.IDType { func (f *eventCollectionFeature) Configure(dda metav1.Object, ddaSpec *v2alpha1.DatadogAgentSpec, _ *v2alpha1.RemoteConfigConfiguration) (reqComp feature.RequiredComponents) { f.owner = dda + // Event collection is configured using the Cluster Agent only; without one there's no + // RBAC to create. + if clusterAgent, ok := ddaSpec.Override[v2alpha1.ClusterAgentComponentName]; ok && apiutils.BoolValue(clusterAgent.Disabled) { + return reqComp + } + // v2alpha1 configures event collection using the cluster agent only // leader election is enabled by default if ddaSpec.Features != nil && ddaSpec.Features.EventCollection != nil && apiutils.BoolValue(ddaSpec.Features.EventCollection.CollectKubernetesEvents) { diff --git a/internal/controller/datadogagent/feature/eventcollection/feature_test.go b/internal/controller/datadogagent/feature/eventcollection/feature_test.go index b83627819a..a73d36bf31 100644 --- a/internal/controller/datadogagent/feature/eventcollection/feature_test.go +++ b/internal/controller/datadogagent/feature/eventcollection/feature_test.go @@ -34,6 +34,14 @@ func Test_eventCollectionFeature_Configure(t *testing.T) { Build(), WantConfigure: false, }, + { + Name: "Event Collection enabled but cluster agent disabled", + DDA: testutils.NewDatadogAgentBuilder(). + WithEventCollectionKubernetesEvents(true). + WithClusterAgentDisabled(true). + Build(), + WantConfigure: false, + }, { Name: "Event Collection enabled", DDA: testutils.NewDatadogAgentBuilder(). From e854f599f1cb68044f937173cbb32a122ff8369a Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Sat, 3 Oct 2026 00:27:39 +0200 Subject: [PATCH 06/28] fix: don't enable admissionController when the cluster agent is disabled The admission controller webhook server runs inside the Cluster Agent; without one there's nothing to back the webhook Service, and the API server would block pod creation trying to reach it. Configure() never checked override.clusterAgent.disabled, so the Service and RBAC were still created and the admission webhooks still registered. Co-Authored-By: Claude Sonnet 5 --- .../feature/admissioncontroller/feature.go | 10 +++++++++- .../feature/admissioncontroller/feature_test.go | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/internal/controller/datadogagent/feature/admissioncontroller/feature.go b/internal/controller/datadogagent/feature/admissioncontroller/feature.go index 6d705b0263..5fcfb53f48 100644 --- a/internal/controller/datadogagent/feature/admissioncontroller/feature.go +++ b/internal/controller/datadogagent/feature/admissioncontroller/feature.go @@ -110,7 +110,15 @@ func (f *admissionControllerFeature) Configure(dda metav1.Object, ddaSpec *v2alp ac := ddaSpec.Features.AdmissionController - if ac != nil && apiutils.BoolValue(ac.Enabled) { + // The admission controller webhook server runs inside the Cluster Agent; without one + // there's nothing to back the webhook Service, and the API server would block pod + // creation trying to reach it. + clusterAgentDisabled := false + if clusterAgent, ok := ddaSpec.Override[v2alpha1.ClusterAgentComponentName]; ok { + clusterAgentDisabled = apiutils.BoolValue(clusterAgent.Disabled) + } + + if !clusterAgentDisabled && ac != nil && apiutils.BoolValue(ac.Enabled) { if ac.Validation != nil && ac.Validation.Enabled != nil { f.validationWebhookConfig = &ValidationConfig{enabled: apiutils.BoolValue(ac.Validation.Enabled)} } diff --git a/internal/controller/datadogagent/feature/admissioncontroller/feature_test.go b/internal/controller/datadogagent/feature/admissioncontroller/feature_test.go index 61a4219cb2..8adfd04bf2 100644 --- a/internal/controller/datadogagent/feature/admissioncontroller/feature_test.go +++ b/internal/controller/datadogagent/feature/admissioncontroller/feature_test.go @@ -36,6 +36,14 @@ func Test_admissionControllerFeature_Configure(t *testing.T) { Build(), WantConfigure: false, }, + { + Name: "Admission Controller enabled but cluster agent disabled", + DDA: testutils.NewDatadogAgentBuilder(). + WithAdmissionControllerEnabled(true). + WithClusterAgentDisabled(true). + Build(), + WantConfigure: false, + }, { Name: "Admission Controller enabled grants CSI driver RBAC unconditionally", DDA: testutils.NewDatadogAgentBuilder(). From 205431c2a6cbfbebb522768e8b447721eab83996 Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Sat, 3 Oct 2026 00:27:44 +0200 Subject: [PATCH 07/28] fix: don't enable appsec injector when the cluster agent is disabled AppSec injection is configured and served by the Cluster Agent. Configure() never checked override.clusterAgent.disabled, so ManageDependencies still created RBAC bound to a nonexistent cluster-agent ServiceAccount. Co-Authored-By: Claude Sonnet 5 --- .../datadogagent/feature/appsec/feature.go | 6 ++++++ .../datadogagent/feature/appsec/feature_test.go | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/internal/controller/datadogagent/feature/appsec/feature.go b/internal/controller/datadogagent/feature/appsec/feature.go index 28fa89808f..84bd228304 100644 --- a/internal/controller/datadogagent/feature/appsec/feature.go +++ b/internal/controller/datadogagent/feature/appsec/feature.go @@ -110,6 +110,12 @@ func (f *appsecFeature) Configure(dda metav1.Object, ddaSpec *v2alpha1.DatadogAg return feature.RequiredComponents{} } + // AppSec injection is configured and served by the Cluster Agent; without one there's + // nothing to create RBAC for. + if clusterAgent, ok := ddaSpec.Override[v2alpha1.ClusterAgentComponentName]; ok && apiutils.BoolValue(clusterAgent.Disabled) { + return feature.RequiredComponents{} + } + var inj *v2alpha1.AppsecInjectorConfig if ddaSpec.Features != nil && ddaSpec.Features.Appsec != nil { inj = ddaSpec.Features.Appsec.Injector diff --git a/internal/controller/datadogagent/feature/appsec/feature_test.go b/internal/controller/datadogagent/feature/appsec/feature_test.go index a7f5c344b0..f40e814111 100644 --- a/internal/controller/datadogagent/feature/appsec/feature_test.go +++ b/internal/controller/datadogagent/feature/appsec/feature_test.go @@ -786,6 +786,21 @@ func TestAppsecFeatureConfigure(t *testing.T) { } } +func TestAppsecFeatureConfigureClusterAgentDisabled(t *testing.T) { + dda := testutils.NewDatadogAgentBuilder(). + WithClusterAgentTag("7.76.0"). + WithClusterAgentDisabled(true). + WithAnnotations(map[string]string{ + AnnotationInjectorEnabled: "true", + }). + Build() + + f := buildAppsecFeature(nil).(*appsecFeature) + reqComp := f.Configure(dda, &dda.Spec, nil) + + assert.False(t, reqComp.IsEnabled(), "AppSec should not be enabled when the cluster agent is disabled") +} + func TestAppsecFeatureManageClusterAgentDisabled(t *testing.T) { // Test that ManageClusterAgent does nothing when feature is disabled dda := testutils.NewDatadogAgentBuilder(). From 1b49e456c8fc9c631c80696fcf8a419091a31b5c Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Sat, 3 Oct 2026 00:27:51 +0200 Subject: [PATCH 08/28] fix: don't enable CSPM when the cluster agent is disabled CSPM dispatches its checks via the Cluster Agent and already ties its node-side (Agent) requirement to the Cluster Agent requirement. Configure() never checked override.clusterAgent.disabled, so ManageDependencies still created RBAC bound to a nonexistent cluster-agent ServiceAccount. Co-Authored-By: Claude Sonnet 5 --- .../controller/datadogagent/feature/cspm/feature.go | 9 ++++++++- .../datadogagent/feature/cspm/feature_test.go | 11 +++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/internal/controller/datadogagent/feature/cspm/feature.go b/internal/controller/datadogagent/feature/cspm/feature.go index c7e2aacc98..be85e190f3 100644 --- a/internal/controller/datadogagent/feature/cspm/feature.go +++ b/internal/controller/datadogagent/feature/cspm/feature.go @@ -80,7 +80,14 @@ func (f *cspmFeature) Configure(dda metav1.Object, ddaSpec *v2alpha1.DatadogAgen cspmConfig := ddaSpec.Features.CSPM - if cspmConfig != nil && apiutils.BoolValue(cspmConfig.Enabled) { + // CSPM dispatches its checks via the Cluster Agent; without one there's no RBAC to + // create and the node-side check has nothing to report through. + clusterAgentDisabled := false + if clusterAgent, ok := ddaSpec.Override[v2alpha1.ClusterAgentComponentName]; ok { + clusterAgentDisabled = apiutils.BoolValue(clusterAgent.Disabled) + } + + if !clusterAgentDisabled && cspmConfig != nil && apiutils.BoolValue(cspmConfig.Enabled) { f.enable = true f.serviceAccountName = constants.GetClusterAgentServiceAccount(dda.GetName(), ddaSpec) diff --git a/internal/controller/datadogagent/feature/cspm/feature_test.go b/internal/controller/datadogagent/feature/cspm/feature_test.go index 412fb5a32e..4878739eac 100644 --- a/internal/controller/datadogagent/feature/cspm/feature_test.go +++ b/internal/controller/datadogagent/feature/cspm/feature_test.go @@ -83,6 +83,17 @@ func Test_cspmFeature_Configure(t *testing.T) { DDA: ddaCSPMDisabled.DeepCopy(), WantConfigure: false, }, + { + Name: "CSPM enabled but cluster agent disabled", + DDA: func() *v2alpha1.DatadogAgent { + dda := ddaCSPMEnabled.DeepCopy() + dda.Spec.Override = map[v2alpha1.ComponentName]*v2alpha1.DatadogAgentComponentOverride{ + v2alpha1.ClusterAgentComponentName: {Disabled: ptr.To(true)}, + } + return dda + }(), + WantConfigure: false, + }, { Name: "CSPM enabled", DDA: ddaCSPMEnabled, From aa607c0e77ec56c8d003152b7781bfa4038413f6 Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Sat, 3 Oct 2026 00:27:56 +0200 Subject: [PATCH 09/28] fix: don't enable externalMetricsServer when the cluster agent is disabled The external metrics server is served by the Cluster Agent. Configure() never checked override.clusterAgent.disabled, so ManageDependencies still created a Service, RBAC, and an APIService with nothing to back them. Co-Authored-By: Claude Sonnet 5 --- .../datadogagent/feature/externalmetrics/feature.go | 6 ++++++ .../feature/externalmetrics/feature_test.go | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/internal/controller/datadogagent/feature/externalmetrics/feature.go b/internal/controller/datadogagent/feature/externalmetrics/feature.go index b8d765b32e..8195863f78 100644 --- a/internal/controller/datadogagent/feature/externalmetrics/feature.go +++ b/internal/controller/datadogagent/feature/externalmetrics/feature.go @@ -77,6 +77,12 @@ func (f *externalMetricsFeature) Configure(dda metav1.Object, ddaSpec *v2alpha1. f.owner = dda em := ddaSpec.Features.ExternalMetricsServer + // The external metrics server is served by the Cluster Agent; without one there's + // nothing to back the APIService or create RBAC for. + if clusterAgent, ok := ddaSpec.Override[v2alpha1.ClusterAgentComponentName]; ok && apiutils.BoolValue(clusterAgent.Disabled) { + return reqComp + } + if em != nil && apiutils.BoolValue(em.Enabled) { // By default, we register the external metrics endpoint f.registerAPIService = em.RegisterAPIService == nil || apiutils.BoolValue(em.RegisterAPIService) diff --git a/internal/controller/datadogagent/feature/externalmetrics/feature_test.go b/internal/controller/datadogagent/feature/externalmetrics/feature_test.go index 7acc951df2..a6b81eceac 100644 --- a/internal/controller/datadogagent/feature/externalmetrics/feature_test.go +++ b/internal/controller/datadogagent/feature/externalmetrics/feature_test.go @@ -105,6 +105,18 @@ func TestExternalMetricsFeature(t *testing.T) { tests.Run(t, buildExternalMetricsFeature) } +func TestExternalMetricsFeature_ClusterAgentDisabled(t *testing.T) { + dda := newAgent(true, true, false, false, v2alpha1.DatadogCredentials{}) + dda.Spec.Override = map[v2alpha1.ComponentName]*v2alpha1.DatadogAgentComponentOverride{ + v2alpha1.ClusterAgentComponentName: {Disabled: ptr.To(true)}, + } + + f := buildExternalMetricsFeature(nil) + reqComp := f.Configure(dda, &dda.Spec, nil) + + assert.False(t, reqComp.IsEnabled(), "external metrics should not be enabled when the cluster agent is disabled") +} + func newAgent(enabled, registerAPIService, useDDM, wpaController bool, secret v2alpha1.DatadogCredentials) *v2alpha1.DatadogAgent { return &v2alpha1.DatadogAgent{ Spec: v2alpha1.DatadogAgentSpec{ From de38866d95253873967c63d55bfac3764d134af9 Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Sat, 3 Oct 2026 00:28:01 +0200 Subject: [PATCH 10/28] fix: don't enable helmCheck when the cluster agent is disabled The helm check only ever runs inside the Cluster Agent (or Cluster Check Runners it dispatches to). Configure() never checked override.clusterAgent.disabled, so ManageDependencies still created RBAC bound to a nonexistent cluster-agent ServiceAccount. Co-Authored-By: Claude Sonnet 5 --- .../controller/datadogagent/feature/helmcheck/feature.go | 6 ++++++ .../datadogagent/feature/helmcheck/feature_test.go | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/internal/controller/datadogagent/feature/helmcheck/feature.go b/internal/controller/datadogagent/feature/helmcheck/feature.go index b9092f9468..9413262875 100644 --- a/internal/controller/datadogagent/feature/helmcheck/feature.go +++ b/internal/controller/datadogagent/feature/helmcheck/feature.go @@ -65,6 +65,12 @@ func (f *helmCheckFeature) Configure(dda metav1.Object, ddaSpec *v2alpha1.Datado f.owner = dda helmCheck := ddaSpec.Features.HelmCheck + // The helm check only ever runs inside the Cluster Agent (or Cluster Check Runners it + // dispatches to); without one there's no RBAC to create. + if clusterAgent, ok := ddaSpec.Override[v2alpha1.ClusterAgentComponentName]; ok && apiutils.BoolValue(clusterAgent.Disabled) { + return reqComp + } + if helmCheck != nil && apiutils.BoolValue(helmCheck.Enabled) { reqComp.ClusterAgent.IsRequired = new(true) reqComp.ClusterAgent.Containers = []apicommon.AgentContainerName{apicommon.ClusterAgentContainerName} diff --git a/internal/controller/datadogagent/feature/helmcheck/feature_test.go b/internal/controller/datadogagent/feature/helmcheck/feature_test.go index 3486c49295..b231f295d0 100644 --- a/internal/controller/datadogagent/feature/helmcheck/feature_test.go +++ b/internal/controller/datadogagent/feature/helmcheck/feature_test.go @@ -40,6 +40,14 @@ func Test_helmCheckFeature_Configure(t *testing.T) { Build(), WantConfigure: false, }, + { + Name: "Helm check enabled but cluster agent disabled", + DDA: testutils.NewDatadogAgentBuilder(). + WithHelmCheckEnabled(true). + WithClusterAgentDisabled(true). + Build(), + WantConfigure: false, + }, { Name: "Helm check enabled", DDA: testutils.NewInitializedDatadogAgentBuilder(resourcesNamespace, resourcesName). From 361c0b907c5f5daa1d2dd6cb95e291c8a3addcf8 Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Sat, 3 Oct 2026 00:28:06 +0200 Subject: [PATCH 11/28] fix: don't enable kubernetesActions when the cluster agent is disabled Kubernetes Actions runs inside the Cluster Agent. Configure() never checked override.clusterAgent.disabled, so ManageDependencies still created RBAC bound to a nonexistent cluster-agent ServiceAccount. Co-Authored-By: Claude Sonnet 5 --- .../datadogagent/feature/kubernetesactions/feature.go | 5 +++++ .../feature/kubernetesactions/feature_test.go | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/internal/controller/datadogagent/feature/kubernetesactions/feature.go b/internal/controller/datadogagent/feature/kubernetesactions/feature.go index dbe186a372..9d0929e38f 100644 --- a/internal/controller/datadogagent/feature/kubernetesactions/feature.go +++ b/internal/controller/datadogagent/feature/kubernetesactions/feature.go @@ -67,6 +67,11 @@ func (f *kubernetesActionsFeature) Configure(dda metav1.Object, ddaSpec *v2alpha return reqComp } + // Kubernetes Actions runs inside the Cluster Agent; without one there's no RBAC to create. + if clusterAgent, ok := ddaSpec.Override[v2alpha1.ClusterAgentComponentName]; ok && apiutils.BoolValue(clusterAgent.Disabled) { + return reqComp + } + if !utils.IsAboveMinVersion(clusterAgentVersion(ddaSpec), ClusterAgentMinVersion, nil) { f.logger.V(1).Info("cluster agent version is too low for Kubernetes Actions", "min", ClusterAgentMinVersion) return reqComp diff --git a/internal/controller/datadogagent/feature/kubernetesactions/feature_test.go b/internal/controller/datadogagent/feature/kubernetesactions/feature_test.go index 86a8a39724..b573a017fa 100644 --- a/internal/controller/datadogagent/feature/kubernetesactions/feature_test.go +++ b/internal/controller/datadogagent/feature/kubernetesactions/feature_test.go @@ -35,6 +35,14 @@ func Test_kubernetesActionsFeature_Configure(t *testing.T) { Build(), WantConfigure: false, }, + { + Name: "KubernetesActions enabled but cluster agent disabled", + DDA: testutils.NewDatadogAgentBuilder(). + WithKubernetesActionsEnabled(true). + WithClusterAgentDisabled(true). + Build(), + WantConfigure: false, + }, { Name: "KubernetesActions enabled but cluster agent version too low", DDA: testutils.NewDatadogAgentBuilder(). From 707fba38edae87ee5550130426f6ce1c52ade379 Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Sat, 3 Oct 2026 00:28:12 +0200 Subject: [PATCH 12/28] fix: don't enable autoscaling when the cluster agent is disabled Autoscaling decisions are computed and served by the Cluster Agent. Configure() never checked override.clusterAgent.disabled, so ManageDependencies still created RBAC bound to a nonexistent cluster-agent ServiceAccount (its RBAC was already tagged for component-scoped cleanup, but creating it at all was still wrong). Co-Authored-By: Claude Sonnet 5 --- .../datadogagent/feature/autoscaling/feature.go | 6 ++++++ .../datadogagent/feature/autoscaling/feature_test.go | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/internal/controller/datadogagent/feature/autoscaling/feature.go b/internal/controller/datadogagent/feature/autoscaling/feature.go index 95351c2a7f..a1fd2709fd 100644 --- a/internal/controller/datadogagent/feature/autoscaling/feature.go +++ b/internal/controller/datadogagent/feature/autoscaling/feature.go @@ -71,6 +71,12 @@ func (f *autoscalingFeature) Configure(dda metav1.Object, ddaSpec *v2alpha1.Data return feature.RequiredComponents{} } + // Autoscaling decisions are computed and served by the Cluster Agent; without one + // there's no RBAC to create. + if clusterAgent, ok := ddaSpec.Override[v2alpha1.ClusterAgentComponentName]; ok && apiutils.BoolValue(clusterAgent.Disabled) { + return feature.RequiredComponents{} + } + if autoscaling.Workload != nil && apiutils.BoolValue(autoscaling.Workload.Enabled) { f.workloadEnabled = true if autoscaling.Workload.InPlaceVerticalScaling != nil { diff --git a/internal/controller/datadogagent/feature/autoscaling/feature_test.go b/internal/controller/datadogagent/feature/autoscaling/feature_test.go index e5a774ce4a..a899ca2665 100644 --- a/internal/controller/datadogagent/feature/autoscaling/feature_test.go +++ b/internal/controller/datadogagent/feature/autoscaling/feature_test.go @@ -119,6 +119,18 @@ func TestAutoscalingFeature(t *testing.T) { tests.Run(t, buildAutoscalingFeature) } +func TestAutoscalingFeature_ClusterAgentDisabled(t *testing.T) { + dda := newAgent(true, true, true, false, false) + dda.Spec.Override = map[v2alpha1.ComponentName]*v2alpha1.DatadogAgentComponentOverride{ + v2alpha1.ClusterAgentComponentName: {Disabled: ptr.To(true)}, + } + + f := buildAutoscalingFeature(nil) + reqComp := f.Configure(dda, &dda.Spec, nil) + + assert.False(t, reqComp.IsEnabled(), "autoscaling should not be enabled when the cluster agent is disabled") +} + func newAgent(workloadEnabled, clusterEnabled, admissionEnabled, clusterSpotEnabled, workloadInPlaceVerticalScalingEnabled bool) *v2alpha1.DatadogAgent { return &v2alpha1.DatadogAgent{ ObjectMeta: metav1.ObjectMeta{ From ef12837a6271d8e68df2ca2148a24465fb49dce9 Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Sat, 3 Oct 2026 00:28:17 +0200 Subject: [PATCH 13/28] fix: don't enable controlPlaneMonitoring when the cluster agent is disabled Control plane monitoring is driven by the Cluster Agent. Configure() never checked override.clusterAgent.disabled, so ManageDependencies still created ConfigMaps for a feature that has nothing to run it. Co-Authored-By: Claude Sonnet 5 --- .../feature/controlplanemonitoring/feature.go | 6 ++++++ .../feature/controlplanemonitoring/feature_test.go | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/internal/controller/datadogagent/feature/controlplanemonitoring/feature.go b/internal/controller/datadogagent/feature/controlplanemonitoring/feature.go index 658bbf3d34..0ebb374793 100644 --- a/internal/controller/datadogagent/feature/controlplanemonitoring/feature.go +++ b/internal/controller/datadogagent/feature/controlplanemonitoring/feature.go @@ -67,6 +67,12 @@ func (f *controlPlaneMonitoringFeature) Configure(dda metav1.Object, ddaSpec *v2 controlPlaneMonitoring := ddaSpec.Features.ControlPlaneMonitoring + // Control plane monitoring is driven by the Cluster Agent; without one there's no RBAC + // or ConfigMap to create. + if clusterAgent, ok := ddaSpec.Override[v2alpha1.ClusterAgentComponentName]; ok && apiutils.BoolValue(clusterAgent.Disabled) { + return reqComp + } + if controlPlaneMonitoring != nil && apiutils.BoolValue(controlPlaneMonitoring.Enabled) { f.enabled = true reqComp.ClusterAgent.IsRequired = new(true) diff --git a/internal/controller/datadogagent/feature/controlplanemonitoring/feature_test.go b/internal/controller/datadogagent/feature/controlplanemonitoring/feature_test.go index c6e27a2cec..e8e84194d9 100644 --- a/internal/controller/datadogagent/feature/controlplanemonitoring/feature_test.go +++ b/internal/controller/datadogagent/feature/controlplanemonitoring/feature_test.go @@ -38,6 +38,14 @@ func Test_controlPlaneMonitoringFeature_Configure(t *testing.T) { Build(), WantConfigure: false, }, + { + Name: "Control Plane Monitoring enabled but cluster agent disabled", + DDA: testutils.NewDatadogAgentBuilder(). + WithControlPlaneMonitoring(true). + WithClusterAgentDisabled(true). + Build(), + WantConfigure: false, + }, { Name: "Control Plane Monitoring enabled with default provider", DDA: testutils.NewInitializedDatadogAgentBuilder(resourcesNamespace, resourcesName). From 9a8cdd8efc9d4b8057c71d9b2dd6c59ed4aaf581 Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Sat, 3 Oct 2026 00:28:24 +0200 Subject: [PATCH 14/28] fix: reflect override.clusterAgent.disabled in DD_CLUSTER_AGENT_ENABLED The default pod template scaffolding (component/agent/default.go) hardcodes DD_CLUSTER_AGENT_ENABLED to "true" on every agent container and the init-config init container, regardless of whether a Cluster Agent is actually being deployed. This is the switch the node agent uses to decide whether to try reaching a cluster agent at all, and was the likely root cause behind several of the per-feature symptoms already fixed on this branch. The enabledefault feature now overrides it to "false" when the Cluster Agent is disabled via override. Co-Authored-By: Claude Sonnet 5 --- .../feature/enabledefault/feature.go | 32 ++++++- .../feature/enabledefault/feature_test.go | 95 +++++++++++++++++++ 2 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 internal/controller/datadogagent/feature/enabledefault/feature_test.go diff --git a/internal/controller/datadogagent/feature/enabledefault/feature.go b/internal/controller/datadogagent/feature/enabledefault/feature.go index 51abd700e3..430e8dabdd 100644 --- a/internal/controller/datadogagent/feature/enabledefault/feature.go +++ b/internal/controller/datadogagent/feature/enabledefault/feature.go @@ -7,10 +7,13 @@ package enabledefault import ( "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" apicommon "github.com/DataDog/datadog-operator/api/datadoghq/common" "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" + apiutils "github.com/DataDog/datadog-operator/api/utils" + "github.com/DataDog/datadog-operator/internal/controller/datadogagent/common" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature" ) @@ -32,8 +35,9 @@ func buildDefaultFeature(options *feature.Options) feature.Feature { } type defaultFeature struct { - owner metav1.Object - logger logr.Logger + owner metav1.Object + clusterAgentDisabled bool + logger logr.Logger } // ID returns the ID of the Feature @@ -45,6 +49,10 @@ func (f *defaultFeature) Configure(dda metav1.Object, ddaSpec *v2alpha1.DatadogA trueValue := true f.owner = dda + if clusterAgent, ok := ddaSpec.Override[v2alpha1.ClusterAgentComponentName]; ok { + f.clusterAgentDisabled = apiutils.BoolValue(clusterAgent.Disabled) + } + return feature.RequiredComponents{ ClusterAgent: feature.RequiredComponent{ IsRequired: &trueValue, @@ -73,12 +81,30 @@ func (f *defaultFeature) ManageClusterAgent(managers feature.PodTemplateManagers // if SingleContainerStrategy is enabled and can be used with the configured feature set. // It should do nothing if the feature doesn't need to configure it. func (f *defaultFeature) ManageSingleContainerNodeAgent(managers feature.PodTemplateManagers) error { - return nil + return f.overrideClusterAgentEnabledEnvVar(managers) } // ManageNodeAgent allows a feature to configure the Node Agent's corev1.PodTemplateSpec // It should do nothing if the feature doesn't need to configure it. func (f *defaultFeature) ManageNodeAgent(managers feature.PodTemplateManagers) error { + return f.overrideClusterAgentEnabledEnvVar(managers) +} + +// overrideClusterAgentEnabledEnvVar corrects DD_CLUSTER_AGENT_ENABLED, which the default pod +// template scaffolding (component/agent/default.go) hardcodes to "true" on every agent +// container regardless of whether a Cluster Agent is actually being deployed. +func (f *defaultFeature) overrideClusterAgentEnabledEnvVar(managers feature.PodTemplateManagers) error { + if !f.clusterAgentDisabled { + return nil + } + envVar := &corev1.EnvVar{ + Name: common.DDClusterAgentEnabled, + Value: "false", + } + managers.EnvVar().AddEnvVar(envVar) + // AddEnvVar only reaches regular containers; the init-config init container builds its + // env vars from the same hardcoded-true default and needs the override too. + managers.EnvVar().AddEnvVarToInitContainer(apicommon.InitConfigContainerName, envVar) return nil } diff --git a/internal/controller/datadogagent/feature/enabledefault/feature_test.go b/internal/controller/datadogagent/feature/enabledefault/feature_test.go new file mode 100644 index 0000000000..3b2799e46c --- /dev/null +++ b/internal/controller/datadogagent/feature/enabledefault/feature_test.go @@ -0,0 +1,95 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +package enabledefault + +import ( + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + + apicommon "github.com/DataDog/datadog-operator/api/datadoghq/common" + "github.com/DataDog/datadog-operator/internal/controller/datadogagent/common" + "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/fake" + mergerfake "github.com/DataDog/datadog-operator/internal/controller/datadogagent/merger/fake" + "github.com/DataDog/datadog-operator/pkg/testutils" +) + +func Test_defaultFeature_ClusterAgentEnabledEnvVar(t *testing.T) { + tests := []struct { + name string + clusterAgentDisabled bool + wantClusterAgentValue string + }{ + { + name: "cluster agent enabled (default)", + clusterAgentDisabled: false, + wantClusterAgentValue: "", + }, + { + name: "cluster agent disabled", + clusterAgentDisabled: true, + wantClusterAgentValue: "false", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + builder := testutils.NewDatadogAgentBuilder() + if tt.clusterAgentDisabled { + builder = builder.WithClusterAgentDisabled(true) + } + dda := builder.Build() + + f := buildDefaultFeature(nil).(*defaultFeature) + f.Configure(dda, &dda.Spec, nil) + + mgr := fake.NewPodTemplateManagers(t, corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "agent"}, + }, + }, + }) + + err := f.ManageNodeAgent(mgr) + assert.NoError(t, err) + + envVars := mgr.EnvVarMgr.EnvVarsByC[mergerfake.AllContainers] + var gotValue string + var found bool + for _, e := range envVars { + if e.Name == common.DDClusterAgentEnabled { + gotValue = e.Value + found = true + } + } + + if tt.clusterAgentDisabled { + assert.True(t, found, "expected DD_CLUSTER_AGENT_ENABLED to be explicitly overridden") + assert.Equal(t, tt.wantClusterAgentValue, gotValue) + } else { + assert.False(t, found, "should not override DD_CLUSTER_AGENT_ENABLED when the cluster agent is enabled") + } + + initEnvVars := mgr.EnvVarMgr.EnvVarsByC[apicommon.InitConfigContainerName] + var initFound bool + var initValue string + for _, e := range initEnvVars { + if e.Name == common.DDClusterAgentEnabled { + initFound = true + initValue = e.Value + } + } + if tt.clusterAgentDisabled { + assert.True(t, initFound, "expected DD_CLUSTER_AGENT_ENABLED to be explicitly overridden on the init-config container") + assert.Equal(t, tt.wantClusterAgentValue, initValue) + } else { + assert.False(t, initFound, "should not override DD_CLUSTER_AGENT_ENABLED on the init-config container when the cluster agent is enabled") + } + }) + } +} From d00766c6707691064e4ba08f20f1ffff2d1f33ab Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Mon, 5 Oct 2026 13:51:07 +0200 Subject: [PATCH 15/28] fix(servicediscovery): write resolved enabled state on single-container strategy too ManageSingleContainerNodeAgent was a no-op, so a disabled service discovery config never reached the unprivileged-single-agent container under SingleContainerStrategy, letting the Agent's own >= 7.78.0 auto-on default silently re-enable it. Co-Authored-By: Claude Sonnet 5 --- .../feature/servicediscovery/feature.go | 19 +++++--- .../feature/servicediscovery/feature_test.go | 44 +++++++++++++++++++ 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/internal/controller/datadogagent/feature/servicediscovery/feature.go b/internal/controller/datadogagent/feature/servicediscovery/feature.go index 14ee46672b..ecb57f7a88 100644 --- a/internal/controller/datadogagent/feature/servicediscovery/feature.go +++ b/internal/controller/datadogagent/feature/servicediscovery/feature.go @@ -108,12 +108,7 @@ func (f *serviceDiscoveryFeature) ManageClusterAgent(managers feature.PodTemplat // ManageNodeAgent allows a feature to configure the Node Agent's corev1.PodTemplateSpec // It should do nothing if the feature doesn't need to configure it. func (f *serviceDiscoveryFeature) ManageNodeAgent(managers feature.PodTemplateManagers) error { - // Add the env var to explicitly reflect the resolved enabled state. - // Otherwise, this feature defaults to enabled in the Agent code for versions >= 7.78.0. - managers.EnvVar().AddEnvVarToContainer(apicommon.CoreAgentContainerName, &corev1.EnvVar{ - Name: common.DDServiceDiscoveryEnabled, - Value: apiutils.BoolToString(&f.enabled), - }) + f.writeEnabledEnvVar(apicommon.CoreAgentContainerName, managers) if !f.enabled { return nil } @@ -171,10 +166,20 @@ func (f *serviceDiscoveryFeature) ManageNodeAgent(managers feature.PodTemplateMa // ManageSingleContainerNodeAgent allows a feature to configure the Agent container for the Node Agent's corev1.PodTemplateSpec // if SingleContainerStrategy is enabled and can be used with the configured feature set. // It should do nothing if the feature doesn't need to configure it. -func (f *serviceDiscoveryFeature) ManageSingleContainerNodeAgent(feature.PodTemplateManagers) error { +func (f *serviceDiscoveryFeature) ManageSingleContainerNodeAgent(managers feature.PodTemplateManagers) error { + f.writeEnabledEnvVar(apicommon.UnprivilegedSingleAgentContainerName, managers) return nil } +// writeEnabledEnvVar explicitly reflects the resolved enabled state on the given container. +// Otherwise, this feature defaults to enabled in the Agent code for versions >= 7.78.0. +func (f *serviceDiscoveryFeature) writeEnabledEnvVar(containerName apicommon.AgentContainerName, managers feature.PodTemplateManagers) { + managers.EnvVar().AddEnvVarToContainer(containerName, &corev1.EnvVar{ + Name: common.DDServiceDiscoveryEnabled, + Value: apiutils.BoolToString(&f.enabled), + }) +} + // ManageClusterChecksRunner allows a feature to configure the ClusterChecksRunner's corev1.PodTemplateSpec // It should do nothing if the feature doesn't need to configure it. func (f *serviceDiscoveryFeature) ManageClusterChecksRunner(feature.PodTemplateManagers) error { diff --git a/internal/controller/datadogagent/feature/servicediscovery/feature_test.go b/internal/controller/datadogagent/feature/servicediscovery/feature_test.go index 0285f31e25..48ce75775b 100644 --- a/internal/controller/datadogagent/feature/servicediscovery/feature_test.go +++ b/internal/controller/datadogagent/feature/servicediscovery/feature_test.go @@ -252,6 +252,50 @@ func Test_serviceDiscoveryFeature_resolveEnabled_InheritsDefaultVersionWhenImage } } +// Test_serviceDiscoveryFeature_ManageSingleContainerNodeAgent verifies that the resolved enabled +// state is also written to the unprivileged single-agent container when SingleContainerStrategy +// is used, since reconciliation calls ManageSingleContainerNodeAgent instead of ManageNodeAgent +// in that mode. Without this, a disabled service-discovery feature would omit +// DD_DISCOVERY_ENABLED=false from that container, letting the Agent's own >= 7.78.0 auto-on +// default re-enable it. +func Test_serviceDiscoveryFeature_ManageSingleContainerNodeAgent(t *testing.T) { + tests := []struct { + name string + enabled bool + want string + }{ + {name: "disabled", enabled: false, want: "false"}, + {name: "enabled", enabled: true, want: "true"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := &serviceDiscoveryFeature{enabled: tt.enabled} + + newPTS := corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: string(apicommon.UnprivilegedSingleAgentContainerName)}, + }, + }, + } + mgr := fake.NewPodTemplateManagers(t, newPTS) + + err := f.ManageSingleContainerNodeAgent(mgr) + assert.NoError(t, err) + + wantEnvVars := []*corev1.EnvVar{ + { + Name: common.DDServiceDiscoveryEnabled, + Value: tt.want, + }, + } + gotEnvVars := mgr.EnvVarMgr.EnvVarsByC[apicommon.UnprivilegedSingleAgentContainerName] + assert.True(t, apiutils.IsEqualStruct(gotEnvVars, wantEnvVars), "single-agent container env vars \ndiff = %s", cmp.Diff(gotEnvVars, wantEnvVars)) + }) + } +} + func serviceDiscoveryEnabledForVersion(version string) bool { return pkgutils.IsAboveMinVersion(version, serviceDiscoveryAutoEnableMinVersion, nil) } From 413800da6b13ef115bccefc4a39291202d6f9aff Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Mon, 5 Oct 2026 15:13:15 +0200 Subject: [PATCH 16/28] test(renderer): add golden fixture for the no-cluster-agent scenario Locks in end-to-end what the preceding feature-level fixes guarantee: with override.clusterAgent.disabled=true, no Cluster Agent Deployment, Service, or RBAC is rendered, and cluster-agent-dependent features (kubeStateMetricsCore, orchestratorExplorer) enabled alongside it self-disable instead of leaving orphaned resources or stale env vars. Co-Authored-By: Claude Sonnet 5 --- .../testutils/renderer/render_golden_test.go | 10 + .../no-cluster-agent-baseline.golden.yaml | 915 ++++++++++++++++++ .../testdata/no-cluster-agent-dda.yaml | 31 + 3 files changed, 956 insertions(+) create mode 100644 internal/controller/testutils/renderer/testdata/golden/no-cluster-agent-baseline.golden.yaml create mode 100644 internal/controller/testutils/renderer/testdata/no-cluster-agent-dda.yaml diff --git a/internal/controller/testutils/renderer/render_golden_test.go b/internal/controller/testutils/renderer/render_golden_test.go index a3335123f1..93f802b215 100644 --- a/internal/controller/testutils/renderer/render_golden_test.go +++ b/internal/controller/testutils/renderer/render_golden_test.go @@ -167,6 +167,16 @@ func TestRender_Golden(t *testing.T) { provider: kubernetes.TalosProvider, golden: "testdata/golden/talos-talos.golden.yaml", }, + { + // Regression fixture for the no-cluster-agent use case: cluster agent + // disabled via override while several cluster-agent-dependent features + // (kubeStateMetricsCore, orchestratorExplorer, serviceDiscovery) are also + // enabled. Locks in that no Cluster Agent Deployment/RBAC/Service is + // rendered and that DD_CLUSTER_AGENT_ENABLED resolves to "false" node-side. + name: "no cluster agent, baseline (no provider)", + ddaFile: "testdata/no-cluster-agent-dda.yaml", + golden: "testdata/golden/no-cluster-agent-baseline.golden.yaml", + }, // OpenShift cases. Provider strings are the real node-label-derived values: // detection yields "openshift-", never a bare "openshift". { diff --git a/internal/controller/testutils/renderer/testdata/golden/no-cluster-agent-baseline.golden.yaml b/internal/controller/testutils/renderer/testdata/golden/no-cluster-agent-baseline.golden.yaml new file mode 100644 index 0000000000..44b74f1f0d --- /dev/null +++ b/internal/controller/testutils/renderer/testdata/golden/no-cluster-agent-baseline.golden.yaml @@ -0,0 +1,915 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/instance: datadog-agent + app.kubernetes.io/managed-by: datadog-operator + app.kubernetes.io/name: datadog-agent-deployment + app.kubernetes.io/part-of: -datadog--agent + app.kubernetes.io/version: "" + operator.datadoghq.com/managed-by-store: "true" + name: datadog-agent-agent + ownerReferences: + - apiVersion: datadoghq.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: DatadogAgentInternal + name: datadog-agent + uid: "" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/instance: datadog-agent + app.kubernetes.io/managed-by: datadog-operator + app.kubernetes.io/name: datadog-agent-deployment + app.kubernetes.io/part-of: -datadog--agent + app.kubernetes.io/version: "" + operator.datadoghq.com/managed-by-store: "true" + name: datadog-agent-agent +rules: +- apiGroups: + - "" + resources: + - endpoints + - nodes/metrics + - nodes/proxy + - nodes/spec + - nodes/stats + verbs: + - get +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get +- apiGroups: + - metrics.eks.amazonaws.com + resources: + - kcm/metrics + - ksh/metrics + verbs: + - get +- nonResourceURLs: + - /metrics + - /metrics/slis + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/instance: datadog-agent + app.kubernetes.io/managed-by: datadog-operator + app.kubernetes.io/name: datadog-agent-deployment + app.kubernetes.io/part-of: -datadog--agent + app.kubernetes.io/version: "" + operator.datadoghq.com/managed-by-store: "true" + name: datadog-agent-agent +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: datadog-agent-agent +subjects: +- kind: ServiceAccount + name: datadog-agent-agent +--- +apiVersion: v1 +data: + token: Z29sZGVuLWZpeGVkLWNsdXN0ZXItYWdlbnQtdG9rZW4tMDAwMDAwMDAwMA== +kind: Secret +metadata: + annotations: + checksum/dca-token-custom-config: 3f6998966e6de63b16d45512e01ccc84 + labels: + app.kubernetes.io/instance: datadog-agent + app.kubernetes.io/managed-by: datadog-operator + app.kubernetes.io/name: datadog-agent-deployment + app.kubernetes.io/part-of: -datadog--agent + app.kubernetes.io/version: "" + operator.datadoghq.com/managed-by-dda-controller: "true" + operator.datadoghq.com/managed-by-store: "true" + name: datadog-agent-token + ownerReferences: + - apiVersion: datadoghq.com/v2alpha1 + blockOwnerDeletion: true + controller: true + kind: DatadogAgent + name: datadog-agent + uid: "" +--- +apiVersion: v1 +data: + install_info: | + --- + install_method: + tool: datadog-operator + tool_version: unknown + installer_version: 0.0.0 +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/instance: datadog-agent + app.kubernetes.io/managed-by: datadog-operator + app.kubernetes.io/name: datadog-agent-deployment + app.kubernetes.io/part-of: -datadog--agent + app.kubernetes.io/version: "" + operator.datadoghq.com/managed-by-dda-controller: "true" + operator.datadoghq.com/managed-by-store: "true" + name: datadog-agent-install-info + ownerReferences: + - apiVersion: datadoghq.com/v2alpha1 + blockOwnerDeletion: true + controller: true + kind: DatadogAgent + name: datadog-agent + uid: "" +--- +apiVersion: v1 +data: + system-probe-seccomp.json: "{\n\t\t\t\"defaultAction\": \"SCMP_ACT_ERRNO\",\n\t\t\t\"syscalls\": + [\n\t\t\t\t{\n\t\t\t\t\"names\": [\"accept4\",\"access\",\"arch_prctl\",\"bind\",\"bpf\",\"brk\",\"capget\",\"capset\",\"chdir\",\"chmod\",\"chown\",\"clock_gettime\",\"clone\",\"clone3\",\"close\",\"close_range\",\"connect\",\"copy_file_range\",\"creat\",\"dup\",\"dup2\",\"dup3\",\"epoll_create\",\"epoll_create1\",\"epoll_ctl\",\"epoll_ctl_old\",\"epoll_pwait\",\"epoll_wait\",\"epoll_wait_old\",\"eventfd\",\"eventfd2\",\"execve\",\"execveat\",\"exit\",\"exit_group\",\"faccessat\",\"faccessat2\",\"fchmod\",\"fchmodat\",\"fchown\",\"fchown32\",\"fchownat\",\"fcntl\",\"fcntl64\",\"fdatasync\",\"flock\",\"fstat\",\"fstat64\",\"fstatfs\",\"fsync\",\"ftruncate\",\"futex\",\"futimens\",\"getcwd\",\"getdents\",\"getdents64\",\"getegid\",\"geteuid\",\"getgid\",\"getgroups\",\"getpeername\",\"getpgid\",\"getpgrp\",\"getpid\",\"getppid\",\"getpriority\",\"getrandom\",\"getresgid\",\"getresgid32\",\"getresuid\",\"getresuid32\",\"getrlimit\",\"getrusage\",\"getsid\",\"getsockname\",\"getsockopt\",\"gettid\",\"gettimeofday\",\"getuid\",\"getxattr\",\"inotify_add_watch\",\"inotify_init\",\"inotify_init1\",\"inotify_rm_watch\",\"ioctl\",\"ipc\",\"listen\",\"lseek\",\"lstat\",\"lstat64\",\"madvise\",\"memfd_create\",\"mkdir\",\"mkdirat\",\"mknod\",\"mknodat\",\"mmap\",\"mmap2\",\"mprotect\",\"mremap\",\"munmap\",\"nanosleep\",\"newfstatat\",\"open\",\"openat\",\"openat2\",\"pause\",\"perf_event_open\",\"pidfd_open\",\"pidfd_send_signal\",\"pipe\",\"pipe2\",\"poll\",\"ppoll\",\"prctl\",\"pread64\",\"prlimit64\",\"pselect6\",\"pwrite64\",\"read\",\"readlink\",\"readlinkat\",\"recvfrom\",\"recvmmsg\",\"recvmsg\",\"rename\",\"renameat\",\"renameat2\",\"restart_syscall\",\"rmdir\",\"rseq\",\"rt_sigaction\",\"rt_sigpending\",\"rt_sigprocmask\",\"rt_sigqueueinfo\",\"rt_sigreturn\",\"rt_sigsuspend\",\"rt_sigtimedwait\",\"rt_tgsigqueueinfo\",\"sched_getaffinity\",\"sched_yield\",\"seccomp\",\"select\",\"semtimedop\",\"send\",\"sendmmsg\",\"sendmsg\",\"sendto\",\"set_robust_list\",\"set_tid_address\",\"setgid\",\"setgid32\",\"setgroups\",\"setgroups32\",\"setitimer\",\"setns\",\"setpgid\",\"setresgid\",\"setresuid\",\"setrlimit\",\"setsid\",\"setsockopt\",\"setuid\",\"setuid32\",\"shutdown\",\"sigaltstack\",\"socket\",\"socketcall\",\"socketpair\",\"stat\",\"stat64\",\"statfs\",\"statx\",\"symlinkat\",\"sysinfo\",\"tgkill\",\"tkill\",\"umask\",\"uname\",\"unlink\",\"unlinkat\",\"utime\",\"utimensat\",\"utimes\",\"wait4\",\"waitid\",\"waitpid\",\"write\",\"writev\"],\n\t\t\t\t\"action\": + \"SCMP_ACT_ALLOW\",\n\t\t\t\t\"args\": null\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\"names\": + [\n\t\t\t\t\t\"setns\"\n\t\t\t\t],\n\t\t\t\t\"action\": \"SCMP_ACT_ALLOW\",\n\t\t\t\t\"args\": + [\n\t\t\t\t\t{\n\t\t\t\t\t\"index\": 1,\n\t\t\t\t\t\"value\": 1073741824,\n\t\t\t\t\t\"valueTwo\": + 0,\n\t\t\t\t\t\"op\": \"SCMP_CMP_EQ\"\n\t\t\t\t\t}\n\t\t\t\t],\n\t\t\t\t\"comment\": + \"\",\n\t\t\t\t\"includes\": {},\n\t\t\t\t\"excludes\": {}\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\"names\": + [\n\t\t\t\t\t\"kill\"\n\t\t\t\t],\n\t\t\t\t\"action\": \"SCMP_ACT_ALLOW\",\n\t\t\t\t\"args\": + [\n\t\t\t\t\t{\n\t\t\t\t\t\"index\": 1,\n\t\t\t\t\t\"value\": 0,\n\t\t\t\t\t\"op\": + \"SCMP_CMP_EQ\"\n\t\t\t\t\t}\n\t\t\t\t],\n\t\t\t\t\"comment\": \"allow process + detection via kill\",\n\t\t\t\t\"includes\": {},\n\t\t\t\t\"excludes\": {}\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t\t" +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/instance: datadog-agent + app.kubernetes.io/managed-by: datadog-operator + app.kubernetes.io/name: datadog-agent-deployment + app.kubernetes.io/part-of: -datadog--agent + app.kubernetes.io/version: "" + operator.datadoghq.com/managed-by-store: "true" + name: datadog-agent-system-probe-seccomp + ownerReferences: + - apiVersion: datadoghq.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: DatadogAgentInternal + name: datadog-agent + uid: "" +--- +apiVersion: v1 +data: + install_id: "" + install_time: "-62135596800" + install_type: k8s_manual +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/instance: datadog-agent + app.kubernetes.io/managed-by: datadog-operator + app.kubernetes.io/name: datadog-agent-deployment + app.kubernetes.io/part-of: -datadog--agent + app.kubernetes.io/version: "" + operator.datadoghq.com/managed-by-dda-controller: "true" + operator.datadoghq.com/managed-by-store: "true" + name: datadog-apm-telemetry-kpi + ownerReferences: + - apiVersion: datadoghq.com/v2alpha1 + blockOwnerDeletion: true + controller: true + kind: DatadogAgent + name: datadog-agent + uid: "" +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/instance: datadog-agent + app.kubernetes.io/managed-by: datadog-operator + app.kubernetes.io/name: datadog-agent-deployment + app.kubernetes.io/part-of: -datadog--agent + app.kubernetes.io/version: "" + operator.datadoghq.com/managed-by-dda-controller: "true" + operator.datadoghq.com/managed-by-store: "true" + name: datadog-agent-agent + ownerReferences: + - apiVersion: datadoghq.com/v2alpha1 + blockOwnerDeletion: true + controller: true + kind: DatadogAgent + name: datadog-agent + uid: "" +spec: + internalTrafficPolicy: Local + ports: + - name: traceport + port: 8126 + protocol: TCP + targetPort: 8126 + - name: dogstatsdport + port: 8125 + protocol: UDP + targetPort: 8125 + selector: + agent.datadoghq.com/component: agent + app.kubernetes.io/part-of: -datadog--agent + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/instance: datadog-agent + app.kubernetes.io/managed-by: datadog-operator + app.kubernetes.io/name: datadog-agent-deployment + app.kubernetes.io/part-of: -datadog--agent + app.kubernetes.io/version: "" + operator.datadoghq.com/managed-by-dda-controller: "true" + operator.datadoghq.com/managed-by-store: "true" + name: datadog-agent-cluster-agent + ownerReferences: + - apiVersion: datadoghq.com/v2alpha1 + blockOwnerDeletion: true + controller: true + kind: DatadogAgent + name: datadog-agent + uid: "" +spec: + internalTrafficPolicy: Cluster + ports: + - port: 5005 + protocol: TCP + targetPort: 5005 + selector: + agent.datadoghq.com/component: cluster-agent + agent.datadoghq.com/name: datadog-agent + type: ClusterIP +--- +apiVersion: datadoghq.com/v1alpha1 +kind: DatadogAgentInternal +metadata: + annotations: + agent.datadoghq.com/ddaispechash: dfd098c5b6d812e3015528cfcd1cdfec + finalizers: + - finalizer.datadoghq.com/datadogagentinternal + labels: + agent.datadoghq.com/datadogagent: datadog-agent + name: datadog-agent + ownerReferences: + - apiVersion: datadoghq.com/v2alpha1 + blockOwnerDeletion: true + controller: true + kind: DatadogAgent + name: datadog-agent + uid: "" +spec: + features: + admissionController: + cwsInstrumentation: + enabled: false + enabled: true + kubernetesAdmissionEvents: + enabled: false + mutateUnlabelled: false + mutation: + enabled: true + serviceName: datadog-admission-controller + validation: + enabled: true + apm: + enabled: true + errorTrackingStandalone: + enabled: false + hostPortConfig: + enabled: false + hostPort: 8126 + instrumentation: + enabled: false + injector: {} + languageDetection: + enabled: true + unixDomainSocketConfig: + enabled: true + path: /var/run/datadog/apm.socket + asm: + iast: + enabled: false + sca: + enabled: false + threats: + enabled: false + clusterChecks: + enabled: true + useClusterChecksRunners: false + controlPlaneMonitoring: + enabled: true + cspm: + enabled: false + cws: + enabled: false + dataPlane: + dogstatsd: + enabled: true + dogstatsd: + hostPortConfig: + enabled: false + nonLocalTraffic: true + originDetectionEnabled: false + unixDomainSocketConfig: + enabled: true + path: /var/run/datadog/dsd.socket + dynamicInstrumentation: + enabled: false + ebpfCheck: + enabled: false + eventCollection: + collectKubernetesEvents: true + externalMetricsServer: + enabled: false + gpu: + enabled: false + helmCheck: + enabled: false + kubeStateMetricsCore: + enabled: true + liveContainerCollection: + enabled: true + liveProcessCollection: + enabled: false + logCollection: + enabled: false + npm: + enabled: false + oomKill: + enabled: false + orchestratorExplorer: + enabled: true + scrubContainers: true + otelCollector: + enabled: false + otlp: + receiver: + protocols: + grpc: + enabled: false + endpoint: 0.0.0.0:4317 + http: + enabled: false + endpoint: 0.0.0.0:4318 + processDiscovery: + enabled: true + prometheusScrape: + enabled: false + remoteConfiguration: + enabled: true + serviceDiscovery: + enabled: true + tcpQueueLength: + enabled: false + usm: + enabled: false + global: + clusterAgentTokenSecret: + keyName: token + secretName: datadog-agent-token + clusterName: test-cluster + containerStrategy: optimized + credentials: + apiSecret: + keyName: api-key + secretName: datadog-secret + appSecret: + keyName: app-key + secretName: datadog-secret + fips: + enabled: false + kubelet: + podResourcesSocketPath: /var/lib/kubelet/pod-resources/ + tlsVerify: false + logLevel: info + registry: registry.datadoghq.com + site: datadoghq.com + useFIPSAgent: false + override: + clusterAgent: + annotations: + checksum/dca-token-custom-config: 3f6998966e6de63b16d45512e01ccc84 + disabled: true + image: + tag: 7.80.0 + nodeAgent: + annotations: + checksum/dca-token-custom-config: 3f6998966e6de63b16d45512e01ccc84 + image: + tag: 7.80.0 +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + annotations: + agent.datadoghq.com/agentspechash: 947c9e5b6e63dfdd3c4425c3298515fa + labels: + agent.datadoghq.com/component: agent + agent.datadoghq.com/name: datadog-agent + app.kubernetes.io/component: agent + app.kubernetes.io/instance: datadog-agent-agent + app.kubernetes.io/managed-by: datadog-operator + app.kubernetes.io/name: datadog-agent-deployment + app.kubernetes.io/part-of: -datadog--agent + app.kubernetes.io/version: "" + name: datadog-agent-agent + ownerReferences: + - apiVersion: datadoghq.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: DatadogAgentInternal + name: datadog-agent + uid: "" +spec: + selector: + matchLabels: + agent.datadoghq.com/component: agent + app.kubernetes.io/instance: datadog-agent-agent + template: + metadata: + annotations: + agent.datadoghq.com/configmapshash: 2e0fb9e7949d41a5 + checksum/dca-token-custom-config: 3f6998966e6de63b16d45512e01ccc84 + container.apparmor.security.beta.kubernetes.io/system-probe: unconfined + labels: + agent.datadoghq.com/component: agent + agent.datadoghq.com/name: datadog-agent + app.kubernetes.io/component: agent + app.kubernetes.io/instance: datadog-agent-agent + app.kubernetes.io/managed-by: datadog-operator + app.kubernetes.io/name: datadog-agent-deployment + app.kubernetes.io/part-of: -datadog--agent + app.kubernetes.io/version: "" + spec: + containers: + - command: + - agent + - run + env: + - name: DD_HEALTH_PORT + value: "5555" + - name: DD_CONTAINER_IMAGE_ENABLED + value: "true" + - name: KUBERNETES + value: "yes" + - name: DD_CLUSTER_AGENT_ENABLED + value: "false" + - name: DD_CLUSTER_AGENT_KUBERNETES_SERVICE_NAME + value: datadog-agent-cluster-agent + - name: DD_KUBERNETES_USE_ENDPOINT_SLICES + value: "true" + - name: DD_CLUSTER_AGENT_TOKEN_NAME + value: datadog-agent-token + - name: DD_AUTH_TOKEN_FILE_PATH + value: /etc/datadog-agent/auth/token + - name: DD_KUBERNETES_KUBELET_HOST + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: DD_CLUSTER_NAME + value: test-cluster + - name: DD_SITE + value: datadoghq.com + - name: DD_LOG_LEVEL + value: info + - name: DD_API_KEY + valueFrom: + secretKeyRef: + key: api-key + name: datadog-secret + - name: DD_APP_KEY + valueFrom: + secretKeyRef: + key: app-key + name: datadog-secret + - name: DD_CLUSTER_AGENT_AUTH_TOKEN + valueFrom: + secretKeyRef: + key: token + name: datadog-agent-token + - name: DD_KUBELET_TLS_VERIFY + value: "false" + - name: DD_ORCHESTRATOR_EXPLORER_ENABLED + value: "false" + - name: DD_LANGUAGE_DETECTION_REPORTING_ENABLED + value: "false" + - name: DD_EXTRA_CONFIG_PROVIDERS + value: clusterchecks endpointschecks + - name: DD_DOGSTATSD_NON_LOCAL_TRAFFIC + value: "true" + - name: DD_DOGSTATSD_SOCKET + value: /var/run/datadog/dsd.socket + - name: DD_PROCESS_CONFIG_RUN_IN_CORE_AGENT_ENABLED + value: "true" + - name: DD_PROCESS_CONFIG_CONTAINER_COLLECTION_ENABLED + value: "true" + - name: DD_PROCESS_CONFIG_PROCESS_DISCOVERY_ENABLED + value: "true" + - name: DD_REMOTE_CONFIGURATION_ENABLED + value: "true" + - name: DD_DISCOVERY_ENABLED + value: "true" + - name: DD_SYSPROBE_SOCKET + value: /var/run/sysprobe/sysprobe.sock + image: registry.datadoghq.com/agent:7.80.0 + livenessProbe: + failureThreshold: 6 + httpGet: + path: /live + port: 5555 + initialDelaySeconds: 15 + periodSeconds: 15 + successThreshold: 1 + timeoutSeconds: 5 + name: agent + ports: + - containerPort: 8125 + name: dogstatsdport + protocol: UDP + readinessProbe: + failureThreshold: 6 + httpGet: + path: /ready + port: 5555 + initialDelaySeconds: 15 + periodSeconds: 15 + successThreshold: 1 + timeoutSeconds: 5 + resources: {} + securityContext: + readOnlyRootFilesystem: true + startupProbe: + failureThreshold: 6 + httpGet: + path: /startup + port: 5555 + initialDelaySeconds: 15 + periodSeconds: 15 + successThreshold: 1 + timeoutSeconds: 5 + volumeMounts: + - mountPath: /var/log/datadog + name: logdatadog + - mountPath: /etc/datadog-agent/auth + name: datadog-agent-auth + - mountPath: /etc/datadog-agent/install_info + name: installinfo + readOnly: true + subPath: install_info + - mountPath: /etc/datadog-agent + name: config + - mountPath: /host/proc + name: procdir + readOnly: true + - mountPath: /host/sys/fs/cgroup + name: cgroups + readOnly: true + - mountPath: /var/run/datadog + name: dsdsocket + - mountPath: /host/var/run + name: runtimesocketdir + readOnly: true + - mountPath: /opt/datadog-agent/run + name: datadogrun + - mountPath: /tmp + name: tmpdir + - mountPath: /etc/passwd + name: passwd + readOnly: true + - mountPath: /var/run/sysprobe + name: sysprobe-socket-dir + readOnly: true + - command: + - trace-loader + - /etc/datadog-agent/datadog.yaml + - trace-agent + - --config=/etc/datadog-agent/datadog.yaml + env: + - name: DD_INSTRUMENTATION_INSTALL_ID + valueFrom: + configMapKeyRef: + key: install_id + name: datadog-apm-telemetry-kpi + - name: DD_INSTRUMENTATION_INSTALL_TIME + valueFrom: + configMapKeyRef: + key: install_time + name: datadog-apm-telemetry-kpi + - name: DD_INSTRUMENTATION_INSTALL_TYPE + valueFrom: + configMapKeyRef: + key: install_type + name: datadog-apm-telemetry-kpi + - name: KUBERNETES + value: "yes" + - name: DD_CLUSTER_AGENT_ENABLED + value: "false" + - name: DD_CLUSTER_AGENT_KUBERNETES_SERVICE_NAME + value: datadog-agent-cluster-agent + - name: DD_KUBERNETES_USE_ENDPOINT_SLICES + value: "true" + - name: DD_CLUSTER_AGENT_TOKEN_NAME + value: datadog-agent-token + - name: DD_AUTH_TOKEN_FILE_PATH + value: /etc/datadog-agent/auth/token + - name: DD_KUBERNETES_KUBELET_HOST + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: DD_CLUSTER_NAME + value: test-cluster + - name: DD_SITE + value: datadoghq.com + - name: DD_LOG_LEVEL + value: info + - name: DD_API_KEY + valueFrom: + secretKeyRef: + key: api-key + name: datadog-secret + - name: DD_APP_KEY + valueFrom: + secretKeyRef: + key: app-key + name: datadog-secret + - name: DD_CLUSTER_AGENT_AUTH_TOKEN + valueFrom: + secretKeyRef: + key: token + name: datadog-agent-token + - name: DD_KUBELET_TLS_VERIFY + value: "false" + - name: DD_APM_ENABLED + value: "true" + - name: DD_APM_NON_LOCAL_TRAFFIC + value: "true" + - name: DD_APM_RECEIVER_SOCKET + value: /var/run/datadog/apm.socket + - name: DD_DOGSTATSD_SOCKET + value: /var/run/datadog/dsd.socket + - name: DD_REMOTE_CONFIGURATION_ENABLED + value: "true" + image: registry.datadoghq.com/agent:7.80.0 + livenessProbe: + initialDelaySeconds: 15 + periodSeconds: 15 + tcpSocket: + port: 8126 + timeoutSeconds: 5 + name: trace-agent + ports: + - containerPort: 8126 + name: traceport + protocol: TCP + resources: {} + securityContext: + readOnlyRootFilesystem: true + volumeMounts: + - mountPath: /var/log/datadog + name: logdatadog + - mountPath: /host/proc + name: procdir + readOnly: true + - mountPath: /host/sys/fs/cgroup + name: cgroups + readOnly: true + - mountPath: /etc/datadog-agent/auth + name: datadog-agent-auth + readOnly: true + - mountPath: /etc/datadog-agent + name: config + - mountPath: /var/run/datadog + name: apmsocket + - mountPath: /host/var/run + name: runtimesocketdir + readOnly: true + - mountPath: /tmp + name: tmpdir + - command: + - system-probe + - --config=/etc/datadog-agent/system-probe.yaml + env: + - name: KUBERNETES + value: "yes" + - name: DD_CLUSTER_AGENT_ENABLED + value: "false" + - name: DD_CLUSTER_AGENT_KUBERNETES_SERVICE_NAME + value: datadog-agent-cluster-agent + - name: DD_KUBERNETES_USE_ENDPOINT_SLICES + value: "true" + - name: DD_CLUSTER_AGENT_TOKEN_NAME + value: datadog-agent-token + - name: DD_AUTH_TOKEN_FILE_PATH + value: /etc/datadog-agent/auth/token + - name: DD_KUBERNETES_KUBELET_HOST + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: DD_CLUSTER_NAME + value: test-cluster + - name: DD_SITE + value: datadoghq.com + - name: DD_LOG_LEVEL + value: info + - name: DD_API_KEY + valueFrom: + secretKeyRef: + key: api-key + name: datadog-secret + - name: DD_APP_KEY + valueFrom: + secretKeyRef: + key: app-key + name: datadog-secret + - name: DD_CLUSTER_AGENT_AUTH_TOKEN + valueFrom: + secretKeyRef: + key: token + name: datadog-agent-token + - name: DD_KUBELET_TLS_VERIFY + value: "false" + - name: DD_DOGSTATSD_SOCKET + value: /var/run/datadog/dsd.socket + - name: DD_REMOTE_CONFIGURATION_ENABLED + value: "true" + - name: DD_DISCOVERY_ENABLED + value: "true" + - name: DD_DISCOVERY_USE_SYSTEM_PROBE_LITE + value: "true" + - name: DD_SYSPROBE_SOCKET + value: /var/run/sysprobe/sysprobe.sock + image: registry.datadoghq.com/agent:7.80.0 + name: system-probe + resources: {} + securityContext: + capabilities: + add: + - CHOWN + - DAC_READ_SEARCH + - IPC_LOCK + - NET_ADMIN + - NET_BROADCAST + - NET_RAW + - SYS_ADMIN + - SYS_PTRACE + - SYS_RESOURCE + readOnlyRootFilesystem: true + seccompProfile: + localhostProfile: system-probe + type: Localhost + volumeMounts: + - mountPath: /var/log/datadog + name: logdatadog + - mountPath: /etc/datadog-agent/auth + name: datadog-agent-auth + readOnly: true + - mountPath: /etc/datadog-agent + name: config + - mountPath: /host/proc + name: procdir + readOnly: true + - mountPath: /opt/datadog-agent/run + name: datadogrun + - mountPath: /tmp + name: tmpdir + - mountPath: /var/run/datadog + name: dsdsocket + - mountPath: /host/sys/fs/cgroup + name: cgroups + readOnly: true + - mountPath: /var/run/sysprobe + name: sysprobe-socket-dir + initContainers: + - args: + - cp -vnr /etc/datadog-agent /opt + command: + - bash + - -c + image: registry.datadoghq.com/agent:7.80.0 + name: init-volume + resources: {} + volumeMounts: + - mountPath: /opt/datadog-agent + name: config + - args: + - for script in $(find /etc/cont-init.d/ -type f -name '*.sh' | sort) ; do + bash $script ; done + command: + - bash + - -c + env: + - name: DD_HEALTH_PORT + value: "5555" + - name: DD_CONTAINER_IMAGE_ENABLED + value: "true" + - name: KUBERNETES + value: "yes" + - name: DD_CLUSTER_AGENT_ENABLED + value: "false" + - name: DD_CLUSTER_AGENT_KUBERNETES_SERVICE_NAME + value: datadog-agent-cluster-agent + - name: DD_KUBERNETES_USE_ENDPOINT_SLICES + value: "true" + - name: DD_CLUSTER_AGENT_TOKEN_NAME + value: datadog-agent-token + - name: DD_AUTH_TOKEN_FILE_PATH + value: /etc/datadog-agent/auth/token + - name: DD_KUBERNETES_KUBELET_HOST + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: DD_DISCOVERY_ENABLED + value: "true" + image: registry.datadoghq.com/agent:7.80.0 + name: init-config + resources: {} + volumeMounts: + - mountPath: /var/log/datadog + name: logdatadog + - mountPath: /checks.d + name: checksd + readOnly: true + - mountPath: /etc/datadog-agent/auth + name: datadog-agent-auth + - mountPath: /conf.d + name: confd + readOnly: true + - mountPath: /etc/datadog-agent + name: config + - mountPath: /host/proc + name: procdir + readOnly: true + - mountPath: /host/var/run + name: runtimesocketdir + readOnly: true + - command: + - cp + - /etc/config/system-probe-seccomp.json + - /host/var/lib/kubelet/seccomp/system-probe + image: registry.datadoghq.com/agent:7.80.0 + name: seccomp-setup + resources: {} + volumeMounts: + - mountPath: /etc/config + name: datadog-agent-security + readOnly: true + - mountPath: /host/var/lib/kubelet/seccomp + name: seccomp-root + securityContext: + runAsUser: 0 + serviceAccountName: datadog-agent-agent + volumes: + - emptyDir: {} + name: logdatadog + - emptyDir: {} + name: datadog-agent-auth + - configMap: + name: datadog-agent-install-info + name: installinfo + - emptyDir: {} + name: checksd + - emptyDir: {} + name: confd + - emptyDir: {} + name: config + - hostPath: + path: /proc + name: procdir + - hostPath: + path: /sys/fs/cgroup + name: cgroups + - hostPath: + path: /var/run/datadog + type: DirectoryOrCreate + name: dsdsocket + - hostPath: + path: /var/run + name: runtimesocketdir + - emptyDir: {} + name: datadogrun + - emptyDir: {} + name: tmpdir + - configMap: + name: datadog-agent-system-probe-seccomp + name: datadog-agent-security + - hostPath: + path: /var/lib/kubelet/seccomp + name: seccomp-root + - hostPath: + path: /var/run/datadog + type: DirectoryOrCreate + name: apmsocket + - hostPath: + path: /etc/passwd + name: passwd + - emptyDir: {} + name: sysprobe-socket-dir + updateStrategy: {} diff --git a/internal/controller/testutils/renderer/testdata/no-cluster-agent-dda.yaml b/internal/controller/testutils/renderer/testdata/no-cluster-agent-dda.yaml new file mode 100644 index 0000000000..19b5abd5e1 --- /dev/null +++ b/internal/controller/testutils/renderer/testdata/no-cluster-agent-dda.yaml @@ -0,0 +1,31 @@ +apiVersion: datadoghq.com/v2alpha1 +kind: DatadogAgent +metadata: + name: datadog-agent +spec: + global: + credentials: + apiSecret: + secretName: datadog-secret + keyName: api-key + appSecret: + secretName: datadog-secret + keyName: app-key + clusterName: test-cluster + # Fixed so the generated cluster-agent token Secret is deterministic for golden files. + clusterAgentToken: golden-fixed-cluster-agent-token-0000000000 + kubelet: + tlsVerify: false + features: + # Enabled on top of the cluster agent being disabled, to lock in that these + # features self-disable (or keep working node-side) instead of silently + # requiring Cluster Agent resources that will never be created. + kubeStateMetricsCore: + enabled: true + orchestratorExplorer: + enabled: true + serviceDiscovery: + enabled: true + override: + clusterAgent: + disabled: true From a46d55b08df2a0c200e63beaffb52d504507da26 Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Mon, 5 Oct 2026 15:31:48 +0200 Subject: [PATCH 17/28] docs(cluster-agent): document running without the Cluster Agent The Cluster Agent setup doc only covered enabling/configuring it, leaving override.clusterAgent.disabled undiscoverable. Add a section plus a matching example manifest. Co-Authored-By: Claude Sonnet 5 --- docs/cluster_agent_setup.md | 24 +++++++++++++++++++ .../datadog-agent-without-clusteragent.yaml | 13 ++++++++++ 2 files changed, 37 insertions(+) create mode 100644 examples/datadogagent/datadog-agent-without-clusteragent.yaml diff --git a/docs/cluster_agent_setup.md b/docs/cluster_agent_setup.md index c707b5ad1c..dc38a70543 100644 --- a/docs/cluster_agent_setup.md +++ b/docs/cluster_agent_setup.md @@ -73,3 +73,27 @@ datadog-agent-hjlbg 1/1 Running 0 33s ``` [1]: https://github.com/DataDog/datadog-operator/blob/main/examples/datadogagent/datadog-agent-with-clusteragent.yaml + +## Running without the Cluster Agent + +The Cluster Agent is optional. If you don't need cluster-level features (Kubernetes State Metrics Core, Admission Controller, Cluster Checks, External Metrics Server, Orchestrator Explorer, etc.), you can disable it and run the node Agent on its own, for example: [`datadog-agent-without-clusteragent.yaml` file][2]: + +```yaml +apiVersion: datadoghq.com/v2alpha1 +kind: DatadogAgent +metadata: + name: datadog +spec: + global: + clusterName: my-example-cluster + credentials: + apiKey: "" + + override: + clusterAgent: + disabled: true +``` + +With `spec.override.clusterAgent.disabled: true`, no Cluster Agent `Deployment`, `Service`, or RBAC is created, and the node Agent is configured to run without one (`DD_CLUSTER_AGENT_ENABLED` is set to `false`). Any feature under `spec.features` that requires the Cluster Agent (for example `kubeStateMetricsCore`, `admissionController`, `clusterChecks`, `externalMetricsServer`, or `orchestratorExplorer`) is automatically left disabled rather than producing a broken, half-configured deployment — remove `override.clusterAgent.disabled` if you later need any of those features. + +[2]: https://github.com/DataDog/datadog-operator/blob/main/examples/datadogagent/datadog-agent-without-clusteragent.yaml diff --git a/examples/datadogagent/datadog-agent-without-clusteragent.yaml b/examples/datadogagent/datadog-agent-without-clusteragent.yaml new file mode 100644 index 0000000000..8832d8632c --- /dev/null +++ b/examples/datadogagent/datadog-agent-without-clusteragent.yaml @@ -0,0 +1,13 @@ +apiVersion: datadoghq.com/v2alpha1 +kind: DatadogAgent +metadata: + name: datadog +spec: + global: + clusterName: my-example-cluster + credentials: + apiKey: + + override: + clusterAgent: + disabled: true From 6f4bf0ac1e5844f6127a7478d2af5f6ed628a623 Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Mon, 5 Oct 2026 15:48:18 +0200 Subject: [PATCH 18/28] fix: don't create DCA token Secret or Service when cluster agent is disabled manageDDADependenciesWithDDAI created the Cluster Agent auth token Secret and Service unconditionally, leaving them behind with no consumer whenever override.clusterAgent.disabled is true. Co-Authored-By: Claude Sonnet 5 --- .../controller/datadogagent/dependencies.go | 29 ++++++---- .../no-cluster-agent-baseline.golden.yaml | 54 ------------------- 2 files changed, 19 insertions(+), 64 deletions(-) diff --git a/internal/controller/datadogagent/dependencies.go b/internal/controller/datadogagent/dependencies.go index 4a15b0f00f..a52b352733 100644 --- a/internal/controller/datadogagent/dependencies.go +++ b/internal/controller/datadogagent/dependencies.go @@ -15,6 +15,7 @@ import ( v1alpha1 "github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1" "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" + apiutils "github.com/DataDog/datadog-operator/api/utils" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/common" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/component/clusteragent" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature" @@ -55,11 +56,25 @@ func (r *Reconciler) manageDDADependenciesWithDDAI(ctx context.Context, logger l return err } - // DCA token - if err := global.AddDCATokenDependencies(logger, instance.GetObjectMeta(), &instance.Spec, &instance.Status, resourceManagers); err != nil { - return err + // DCA token and Service are only needed when a Cluster Agent is actually + // going to be deployed; otherwise they're resources without a consumer. + clusterAgentDisabled := false + if override, ok := instance.Spec.Override[v2alpha1.ClusterAgentComponentName]; ok { + clusterAgentDisabled = apiutils.BoolValue(override.Disabled) + } + if !clusterAgentDisabled { + // DCA token + if err := global.AddDCATokenDependencies(logger, instance.GetObjectMeta(), &instance.Spec, &instance.Status, resourceManagers); err != nil { + return err + } + ensureAutoGeneratedTokenInStatus(instance, newDDAStatus, resourceManagers, logger) + + // DCA service + service := clusteragent.GetClusterAgentService(instance) + if err := resourceManagers.ServiceManager().AddService(service.Name, service.Namespace, service.Spec.Selector, service.Spec.Ports, service.Spec.InternalTrafficPolicy); err != nil { + return err + } } - ensureAutoGeneratedTokenInStatus(instance, newDDAStatus, resourceManagers, logger) // APM Telemetry // ConfigMap stores DatadogAgent resource UID and creation time whether or not DatadogAgentInternal is enabled. @@ -67,12 +82,6 @@ func (r *Reconciler) manageDDADependenciesWithDDAI(ctx context.Context, logger l return err } - // DCA service - service := clusteragent.GetClusterAgentService(instance) - if err := resourceManagers.ServiceManager().AddService(service.Name, service.Namespace, service.Spec.Selector, service.Spec.Ports, service.Spec.InternalTrafficPolicy); err != nil { - return err - } - // Dependencies that can get configs from DDA and DDAI // Example: agent local service for APM, DSD, and OTLP if err := r.addDDASharedDependencies(instance, ddais, resourceManagers); err != nil { diff --git a/internal/controller/testutils/renderer/testdata/golden/no-cluster-agent-baseline.golden.yaml b/internal/controller/testutils/renderer/testdata/golden/no-cluster-agent-baseline.golden.yaml index 44b74f1f0d..cdee7d5ee1 100644 --- a/internal/controller/testutils/renderer/testdata/golden/no-cluster-agent-baseline.golden.yaml +++ b/internal/controller/testutils/renderer/testdata/golden/no-cluster-agent-baseline.golden.yaml @@ -78,30 +78,6 @@ subjects: name: datadog-agent-agent --- apiVersion: v1 -data: - token: Z29sZGVuLWZpeGVkLWNsdXN0ZXItYWdlbnQtdG9rZW4tMDAwMDAwMDAwMA== -kind: Secret -metadata: - annotations: - checksum/dca-token-custom-config: 3f6998966e6de63b16d45512e01ccc84 - labels: - app.kubernetes.io/instance: datadog-agent - app.kubernetes.io/managed-by: datadog-operator - app.kubernetes.io/name: datadog-agent-deployment - app.kubernetes.io/part-of: -datadog--agent - app.kubernetes.io/version: "" - operator.datadoghq.com/managed-by-dda-controller: "true" - operator.datadoghq.com/managed-by-store: "true" - name: datadog-agent-token - ownerReferences: - - apiVersion: datadoghq.com/v2alpha1 - blockOwnerDeletion: true - controller: true - kind: DatadogAgent - name: datadog-agent - uid: "" ---- -apiVersion: v1 data: install_info: | --- @@ -218,36 +194,6 @@ spec: app.kubernetes.io/part-of: -datadog--agent type: ClusterIP --- -apiVersion: v1 -kind: Service -metadata: - labels: - app.kubernetes.io/instance: datadog-agent - app.kubernetes.io/managed-by: datadog-operator - app.kubernetes.io/name: datadog-agent-deployment - app.kubernetes.io/part-of: -datadog--agent - app.kubernetes.io/version: "" - operator.datadoghq.com/managed-by-dda-controller: "true" - operator.datadoghq.com/managed-by-store: "true" - name: datadog-agent-cluster-agent - ownerReferences: - - apiVersion: datadoghq.com/v2alpha1 - blockOwnerDeletion: true - controller: true - kind: DatadogAgent - name: datadog-agent - uid: "" -spec: - internalTrafficPolicy: Cluster - ports: - - port: 5005 - protocol: TCP - targetPort: 5005 - selector: - agent.datadoghq.com/component: cluster-agent - agent.datadoghq.com/name: datadog-agent - type: ClusterIP ---- apiVersion: datadoghq.com/v1alpha1 kind: DatadogAgentInternal metadata: From 5d915997ba3bd6a84463793fea77b53ce79ad5ea Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Mon, 5 Oct 2026 15:50:15 +0200 Subject: [PATCH 19/28] fix: don't write DD_CLUSTER_AGENT_AUTH_TOKEN when cluster agent is disabled requiredComponents.ClusterAgent.IsEnabled() reflects merged feature requirements only, not override.clusterAgent.disabled, so the node agent kept a secretKeyRef to the DCA token Secret even after the preceding commit stopped creating that Secret, which would have broken pod startup. Co-Authored-By: Claude Sonnet 5 --- .../controller/datadogagent/global/global.go | 12 ++++++++++-- .../no-cluster-agent-baseline.golden.yaml | 17 +---------------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/internal/controller/datadogagent/global/global.go b/internal/controller/datadogagent/global/global.go index 65503f5524..3c289cb938 100644 --- a/internal/controller/datadogagent/global/global.go +++ b/internal/controller/datadogagent/global/global.go @@ -240,8 +240,16 @@ func applyGlobalSettings(logger logr.Logger, manager feature.PodTemplateManagers // Credentials credentialResource(ddaMeta, ddaSpec, manager) - // DCA token - if requiredComponents.ClusterAgent.IsEnabled() { + // DCA token. requiredComponents.ClusterAgent.IsEnabled() reflects feature + // requirements only; it stays true even when override.clusterAgent.disabled + // is set, since that override is resolved later at the component-reconcile + // layer. Check it directly here so the node agent doesn't reference a token + // Secret that will never be created. + clusterAgentDisabled := false + if override, ok := ddaSpec.Override[v2alpha1.ClusterAgentComponentName]; ok { + clusterAgentDisabled = apiutils.BoolValue(override.Disabled) + } + if requiredComponents.ClusterAgent.IsEnabled() && !clusterAgentDisabled { dcaTokenResource(ddaMeta, ddaSpec, resourcesManager, manager) } diff --git a/internal/controller/testutils/renderer/testdata/golden/no-cluster-agent-baseline.golden.yaml b/internal/controller/testutils/renderer/testdata/golden/no-cluster-agent-baseline.golden.yaml index cdee7d5ee1..068f165692 100644 --- a/internal/controller/testutils/renderer/testdata/golden/no-cluster-agent-baseline.golden.yaml +++ b/internal/controller/testutils/renderer/testdata/golden/no-cluster-agent-baseline.golden.yaml @@ -356,7 +356,7 @@ apiVersion: apps/v1 kind: DaemonSet metadata: annotations: - agent.datadoghq.com/agentspechash: 947c9e5b6e63dfdd3c4425c3298515fa + agent.datadoghq.com/agentspechash: 7dbf4e66d49f08443630eacf98a5245d labels: agent.datadoghq.com/component: agent agent.datadoghq.com/name: datadog-agent @@ -436,11 +436,6 @@ spec: secretKeyRef: key: app-key name: datadog-secret - - name: DD_CLUSTER_AGENT_AUTH_TOKEN - valueFrom: - secretKeyRef: - key: token - name: datadog-agent-token - name: DD_KUBELET_TLS_VERIFY value: "false" - name: DD_ORCHESTRATOR_EXPLORER_ENABLED @@ -586,11 +581,6 @@ spec: secretKeyRef: key: app-key name: datadog-secret - - name: DD_CLUSTER_AGENT_AUTH_TOKEN - valueFrom: - secretKeyRef: - key: token - name: datadog-agent-token - name: DD_KUBELET_TLS_VERIFY value: "false" - name: DD_APM_ENABLED @@ -675,11 +665,6 @@ spec: secretKeyRef: key: app-key name: datadog-secret - - name: DD_CLUSTER_AGENT_AUTH_TOKEN - valueFrom: - secretKeyRef: - key: token - name: datadog-agent-token - name: DD_KUBELET_TLS_VERIFY value: "false" - name: DD_DOGSTATSD_SOCKET From 4d4f619eb7cf4a5d4716a05cbe7bd80c255a0c98 Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Mon, 5 Oct 2026 18:57:44 +0200 Subject: [PATCH 20/28] fix: don't flag no-cluster-agent as an override conflict enabledefault always set ClusterAgent.IsRequired=true, so merging it with override.clusterAgent.disabled made checkComponentEnabledWithOverride treat every no-Cluster-Agent DatadogAgent as a feature/override conflict, persisting an OverrideReconcileConflict status condition on an otherwise supported configuration. Resolve to an explicit false, the documented "component is disabled" sentinel, when the override disables it. Co-Authored-By: Claude Sonnet 5 --- .../feature/enabledefault/feature.go | 17 ++++++--- .../feature/enabledefault/feature_test.go | 36 +++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/internal/controller/datadogagent/feature/enabledefault/feature.go b/internal/controller/datadogagent/feature/enabledefault/feature.go index 430e8dabdd..cb33ba6b94 100644 --- a/internal/controller/datadogagent/feature/enabledefault/feature.go +++ b/internal/controller/datadogagent/feature/enabledefault/feature.go @@ -53,11 +53,20 @@ func (f *defaultFeature) Configure(dda metav1.Object, ddaSpec *v2alpha1.DatadogA f.clusterAgentDisabled = apiutils.BoolValue(clusterAgent.Disabled) } + // IsRequired: false (rather than nil) tells the merge that the Cluster Agent is + // explicitly disabled via override, so checkComponentEnabledWithOverride resolves + // this as a clean disable instead of an override/feature conflict, as long as no + // other feature still requires it. + clusterAgentRequired := feature.RequiredComponent{ + IsRequired: &trueValue, + Containers: []apicommon.AgentContainerName{apicommon.ClusterAgentContainerName}, + } + if f.clusterAgentDisabled { + clusterAgentRequired = feature.RequiredComponent{IsRequired: new(false)} + } + return feature.RequiredComponents{ - ClusterAgent: feature.RequiredComponent{ - IsRequired: &trueValue, - Containers: []apicommon.AgentContainerName{apicommon.ClusterAgentContainerName}, - }, + ClusterAgent: clusterAgentRequired, Agent: feature.RequiredComponent{ IsRequired: &trueValue, Containers: []apicommon.AgentContainerName{}, diff --git a/internal/controller/datadogagent/feature/enabledefault/feature_test.go b/internal/controller/datadogagent/feature/enabledefault/feature_test.go index 3b2799e46c..bb64d6ebde 100644 --- a/internal/controller/datadogagent/feature/enabledefault/feature_test.go +++ b/internal/controller/datadogagent/feature/enabledefault/feature_test.go @@ -93,3 +93,39 @@ func Test_defaultFeature_ClusterAgentEnabledEnvVar(t *testing.T) { }) } } + +// Test_defaultFeature_Configure_ClusterAgentRequired verifies that, when the Cluster Agent is +// disabled via override, Configure resolves ClusterAgent.IsRequired to an explicit false rather +// than leaving it true (or nil). An explicit false merges with any other feature's requirement +// as "component is disabled" rather than "feature wants it but override disables it", so +// checkComponentEnabledWithOverride resolves this as a clean disable instead of persisting an +// OverrideReconcileConflict status condition on an otherwise legitimate no-Cluster-Agent setup. +func Test_defaultFeature_Configure_ClusterAgentRequired(t *testing.T) { + tests := []struct { + name string + clusterAgentDisabled bool + wantIsEnabled bool + }{ + {name: "cluster agent enabled (default)", clusterAgentDisabled: false, wantIsEnabled: true}, + {name: "cluster agent disabled", clusterAgentDisabled: true, wantIsEnabled: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + builder := testutils.NewDatadogAgentBuilder() + if tt.clusterAgentDisabled { + builder = builder.WithClusterAgentDisabled(true) + } + dda := builder.Build() + + f := buildDefaultFeature(nil) + reqComp := f.Configure(dda, &dda.Spec, nil) + + assert.Equal(t, tt.wantIsEnabled, reqComp.ClusterAgent.IsEnabled()) + if tt.clusterAgentDisabled { + assert.NotNil(t, reqComp.ClusterAgent.IsRequired) + assert.False(t, *reqComp.ClusterAgent.IsRequired) + } + }) + } +} From 693ae2a3136e803cbb08dcb6f990e9fbcb8d793b Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Mon, 5 Oct 2026 18:59:50 +0200 Subject: [PATCH 21/28] fix: don't enable clusterChecks when the cluster agent is disabled clusterChecks defaults to enabled and had no clusterAgent.disabled gate, so the node agent kept DD_EXTRA_CONFIG_PROVIDERS pointing at cluster-checks/endpoints-checks providers that the (nonexistent) Cluster Agent would never dispatch configs for. Co-Authored-By: Claude Sonnet 5 --- .../datadogagent/feature/clusterchecks/feature.go | 4 ++++ .../datadogagent/feature/clusterchecks/feature_test.go | 8 ++++++++ .../testdata/golden/no-cluster-agent-baseline.golden.yaml | 4 +--- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/internal/controller/datadogagent/feature/clusterchecks/feature.go b/internal/controller/datadogagent/feature/clusterchecks/feature.go index 08d0bdb47f..d1aeda6832 100644 --- a/internal/controller/datadogagent/feature/clusterchecks/feature.go +++ b/internal/controller/datadogagent/feature/clusterchecks/feature.go @@ -58,6 +58,10 @@ func (f *clusterChecksFeature) ID() feature.IDType { } func (f *clusterChecksFeature) Configure(dda metav1.Object, ddaSpec *v2alpha1.DatadogAgentSpec, _ *v2alpha1.RemoteConfigConfiguration) (reqComp feature.RequiredComponents) { + if clusterAgent, ok := ddaSpec.Override[v2alpha1.ClusterAgentComponentName]; ok && apiutils.BoolValue(clusterAgent.Disabled) { + return reqComp + } + if apiutils.BoolValue(ddaSpec.Features.ClusterChecks.Enabled) { f.updateConfigHash(dda, ddaSpec) f.owner = dda diff --git a/internal/controller/datadogagent/feature/clusterchecks/feature_test.go b/internal/controller/datadogagent/feature/clusterchecks/feature_test.go index 4dced2ec45..7f056bcbbb 100644 --- a/internal/controller/datadogagent/feature/clusterchecks/feature_test.go +++ b/internal/controller/datadogagent/feature/clusterchecks/feature_test.go @@ -68,6 +68,14 @@ func TestClusterChecksFeature(t *testing.T) { ClusterAgent: test.NewDefaultComponentTest().WithWantFunc(wantClusterAgentHasExpectedEnvsAndChecksum), Agent: testAgentHasExpectedEnvsWithNoRunners(apicommon.CoreAgentContainerName), }, + { + Name: "cluster checks enabled but cluster agent disabled", + DDA: testutils.NewDatadogAgentBuilder(). + WithClusterChecksEnabled(true). + WithClusterAgentDisabled(true). + Build(), + WantConfigure: false, + }, { Name: "cluster checks enabled and runners not enabled with single container strategy", DDA: testutils.NewDatadogAgentBuilder(). diff --git a/internal/controller/testutils/renderer/testdata/golden/no-cluster-agent-baseline.golden.yaml b/internal/controller/testutils/renderer/testdata/golden/no-cluster-agent-baseline.golden.yaml index 068f165692..29c9cd60da 100644 --- a/internal/controller/testutils/renderer/testdata/golden/no-cluster-agent-baseline.golden.yaml +++ b/internal/controller/testutils/renderer/testdata/golden/no-cluster-agent-baseline.golden.yaml @@ -356,7 +356,7 @@ apiVersion: apps/v1 kind: DaemonSet metadata: annotations: - agent.datadoghq.com/agentspechash: 7dbf4e66d49f08443630eacf98a5245d + agent.datadoghq.com/agentspechash: 31c33ac002c7494d749cd50ac5942818 labels: agent.datadoghq.com/component: agent agent.datadoghq.com/name: datadog-agent @@ -442,8 +442,6 @@ spec: value: "false" - name: DD_LANGUAGE_DETECTION_REPORTING_ENABLED value: "false" - - name: DD_EXTRA_CONFIG_PROVIDERS - value: clusterchecks endpointschecks - name: DD_DOGSTATSD_NON_LOCAL_TRAFFIC value: "true" - name: DD_DOGSTATSD_SOCKET From 4d1fd459122ef083f9f5dc28c19b7c0f59fc99a5 Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Tue, 6 Oct 2026 15:10:42 +0200 Subject: [PATCH 22/28] fix: don't enable cluster-side private action runner when the cluster agent is disabled privateactionrunner's Cluster Agent config is annotation-gated rather than spec.features-gated, so it was missed in the earlier feature audit. It had no clusterAgent.disabled check, so it kept requiring a Cluster Agent and creating its ConfigMap (and optional RBAC) with nothing to deploy them onto. Co-Authored-By: Claude Sonnet 5 --- .../feature/privateactionrunner/feature.go | 11 +++++-- .../privateactionrunner/feature_test.go | 30 +++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/internal/controller/datadogagent/feature/privateactionrunner/feature.go b/internal/controller/datadogagent/feature/privateactionrunner/feature.go index 35c5d03c6d..c99bb4e770 100644 --- a/internal/controller/datadogagent/feature/privateactionrunner/feature.go +++ b/internal/controller/datadogagent/feature/privateactionrunner/feature.go @@ -15,6 +15,7 @@ import ( apicommon "github.com/DataDog/datadog-operator/api/datadoghq/common" "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" + apiutils "github.com/DataDog/datadog-operator/api/utils" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/common" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/experimental" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature" @@ -101,8 +102,14 @@ func (f *privateActionRunnerFeature) Configure(dda metav1.Object, ddaSpec *v2alp } } - // Check for Cluster Agent configuration (annotation-based) - if featureutils.HasFeatureEnableAnnotation(dda, featureutils.EnableClusterAgentPrivateActionRunnerAnnotation) { + // Check for Cluster Agent configuration (annotation-based). Skipped when the Cluster Agent + // is disabled via override: otherwise this would still require a Cluster Agent and create + // its ConfigMap/RBAC with nothing to deploy them onto. + clusterAgentDisabled := false + if override, ok := ddaSpec.Override[v2alpha1.ClusterAgentComponentName]; ok { + clusterAgentDisabled = apiutils.BoolValue(override.Disabled) + } + if !clusterAgentDisabled && featureutils.HasFeatureEnableAnnotation(dda, featureutils.EnableClusterAgentPrivateActionRunnerAnnotation) { // Use config data from annotation directly, or fall back to default if configData, ok := featureutils.GetFeatureConfigAnnotation(dda, featureutils.ClusterAgentPrivateActionRunnerConfigDataAnnotation); ok { f.clusterConfigData = configData diff --git a/internal/controller/datadogagent/feature/privateactionrunner/feature_test.go b/internal/controller/datadogagent/feature/privateactionrunner/feature_test.go index 3512977ee4..c69b80ea77 100644 --- a/internal/controller/datadogagent/feature/privateactionrunner/feature_test.go +++ b/internal/controller/datadogagent/feature/privateactionrunner/feature_test.go @@ -711,6 +711,36 @@ func Test_privateActionRunnerFeature_ConfigureClusterAgent(t *testing.T) { } } +// Test_privateActionRunnerFeature_ConfigureClusterAgent_ClusterAgentDisabled verifies that the +// cluster-side annotation is ignored when the Cluster Agent is disabled via override, so this +// feature doesn't require a Cluster Agent or create its ConfigMap/RBAC with nothing to deploy +// them onto. +func Test_privateActionRunnerFeature_ConfigureClusterAgent_ClusterAgentDisabled(t *testing.T) { + f := buildPrivateActionRunnerFeature(nil) + dda := &v2alpha1.DatadogAgent{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "cluster-agent.datadoghq.com/private-action-runner-enabled": "true", + }, + }, + } + ddaSpec := &v2alpha1.DatadogAgentSpec{ + Override: map[v2alpha1.ComponentName]*v2alpha1.DatadogAgentComponentOverride{ + v2alpha1.ClusterAgentComponentName: { + Disabled: new(true), + }, + }, + } + + reqComp := f.Configure(dda, ddaSpec, nil) + + assert.False(t, reqComp.ClusterAgent.IsEnabled()) + + parFeat, ok := f.(*privateActionRunnerFeature) + require.True(t, ok) + assert.Nil(t, parFeat.clusterConfig, "clusterConfig should not be set when the cluster agent is disabled") +} + func Test_privateActionRunnerFeature_ManageClusterAgent_ConfigMap(t *testing.T) { testScheme := runtime.NewScheme() _ = corev1.AddToScheme(testScheme) From 6849a2186a2c1755f7fccc50c2a7481212e10ad5 Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Tue, 6 Oct 2026 15:19:57 +0200 Subject: [PATCH 23/28] docs: flag remaining unguarded ClusterAgent.IsRequired as fragile, not bugs asm, prometheusscrape and remoteconfig still set ClusterAgent.IsRequired unconditionally without checking override.clusterAgent.disabled. Unlike the 20 previously fixed instances, these are currently harmless: none creates orphaned dependencies, and enabledefault's explicit false already wins in the merge. Document the invariant they rely on so a future change to the merge order or to enabledefault doesn't silently reintroduce the override-conflict bug here. Co-Authored-By: Claude Sonnet 5 --- internal/controller/datadogagent/feature/asm/feature.go | 5 +++++ .../datadogagent/feature/prometheusscrape/feature.go | 6 ++++++ .../datadogagent/feature/remoteconfig/feature.go | 5 +++++ internal/controller/datadogagent/feature/types.go | 8 ++++++++ 4 files changed, 24 insertions(+) diff --git a/internal/controller/datadogagent/feature/asm/feature.go b/internal/controller/datadogagent/feature/asm/feature.go index 006b2a813a..74cd4bb022 100644 --- a/internal/controller/datadogagent/feature/asm/feature.go +++ b/internal/controller/datadogagent/feature/asm/feature.go @@ -61,6 +61,11 @@ func (f *asmFeature) Configure(_ metav1.Object, ddaSpec *v2alpha1.DatadogAgentSp f.scaEnabled = apiutils.BoolValue(asm.SCA.Enabled) // The cluster agent and the admission controller are required for the ASM feature. + // This is not gated on override.clusterAgent.disabled: it's safe only because + // enabledefault's explicit false wins over this true in the merge (see + // feature/types.go's merge()) when the Cluster Agent is disabled. ManageDependencies + // is a no-op and ManageClusterAgent only writes env vars to a pod template that's + // never deployed, so there's nothing to orphan. return feature.RequiredComponents{ ClusterAgent: feature.RequiredComponent{ IsRequired: new(true), diff --git a/internal/controller/datadogagent/feature/prometheusscrape/feature.go b/internal/controller/datadogagent/feature/prometheusscrape/feature.go index 4161d78bf1..f74f7a4005 100644 --- a/internal/controller/datadogagent/feature/prometheusscrape/feature.go +++ b/internal/controller/datadogagent/feature/prometheusscrape/feature.go @@ -64,6 +64,12 @@ func (f *prometheusScrapeFeature) Configure(_ metav1.Object, ddaSpec *v2alpha1.D apicommon.CoreAgentContainerName, }, }, + // Not gated on override.clusterAgent.disabled: the Agent requirement above must + // stay set regardless (Prometheus scrape still runs node-side), and this is + // otherwise safe only because enabledefault's explicit false wins over this + // true in the merge (see feature/types.go's merge()). ManageDependencies is a + // no-op and ManageClusterAgent only writes an env var to a pod template that's + // never deployed, so there's nothing to orphan. ClusterAgent: feature.RequiredComponent{ IsRequired: new(true), Containers: []apicommon.AgentContainerName{ diff --git a/internal/controller/datadogagent/feature/remoteconfig/feature.go b/internal/controller/datadogagent/feature/remoteconfig/feature.go index ff472d5dd8..8a00615c87 100644 --- a/internal/controller/datadogagent/feature/remoteconfig/feature.go +++ b/internal/controller/datadogagent/feature/remoteconfig/feature.go @@ -73,6 +73,11 @@ func (f *rcFeature) Configure(_ metav1.Object, ddaSpec *v2alpha1.DatadogAgentSpe // Ref: https://github.com/DataDog/datadog-operator/blob/c4b6e498048a11fbe99d1ea51d2870c6be578799/internal/controller/datadogagent/feature/types.go#L37 if f.enabled { reqComp.Agent.IsRequired = ddaSpec.Features.RemoteConfiguration.Enabled + // Not gated on override.clusterAgent.disabled: the Agent requirement above must + // stay set regardless (remote config still runs node-side), and this is + // otherwise safe only because enabledefault's explicit false wins over this + // true in the merge (see feature/types.go's merge()). ManageDependencies is a + // no-op, so there's nothing to orphan. reqComp.ClusterAgent.IsRequired = ddaSpec.Features.RemoteConfiguration.Enabled } } diff --git a/internal/controller/datadogagent/feature/types.go b/internal/controller/datadogagent/feature/types.go index c5efd26b6a..316fe268f7 100644 --- a/internal/controller/datadogagent/feature/types.go +++ b/internal/controller/datadogagent/feature/types.go @@ -99,6 +99,14 @@ func (rc *RequiredComponent) Merge(in *RequiredComponent) *RequiredComponent { return rc } +// merge priority: false > true > nil. +// +// This is relied upon for the no-Cluster-Agent configuration (override.clusterAgent.disabled): +// the enabledefault feature resolves its ClusterAgent.IsRequired to an explicit false in that +// case, which wins here over any other feature that still unconditionally requires the Cluster +// Agent (e.g. asm, prometheusscrape, remoteconfig), keeping the merged result correctly +// "not required" even for features that don't individually check the override. Those features +// are only safe because of this merge order — if it changes, re-audit them. func merge(a, b *bool) *bool { if a == nil && b == nil { return nil From 7207e9724b6a4d0e83960769276fa03742aec092 Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Tue, 6 Oct 2026 15:27:27 +0200 Subject: [PATCH 24/28] fix: don't require the cluster agent for ASM when it is disabled asm.Configure read admissionController.enabled directly rather than checking override.clusterAgent.disabled, so it kept requiring the Cluster Agent even when disabled. Co-Authored-By: Claude Sonnet 5 --- internal/controller/datadogagent/feature/asm/feature.go | 9 ++++----- .../controller/datadogagent/feature/asm/feature_test.go | 9 +++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/internal/controller/datadogagent/feature/asm/feature.go b/internal/controller/datadogagent/feature/asm/feature.go index 74cd4bb022..6dfde6a6c3 100644 --- a/internal/controller/datadogagent/feature/asm/feature.go +++ b/internal/controller/datadogagent/feature/asm/feature.go @@ -56,16 +56,15 @@ func (f *asmFeature) Configure(_ metav1.Object, ddaSpec *v2alpha1.DatadogAgentSp return feature.RequiredComponents{} } + if clusterAgent, ok := ddaSpec.Override[v2alpha1.ClusterAgentComponentName]; ok && apiutils.BoolValue(clusterAgent.Disabled) { + return feature.RequiredComponents{} + } + f.threatsEnabled = apiutils.BoolValue(asm.Threats.Enabled) f.iastEnabled = apiutils.BoolValue(asm.IAST.Enabled) f.scaEnabled = apiutils.BoolValue(asm.SCA.Enabled) // The cluster agent and the admission controller are required for the ASM feature. - // This is not gated on override.clusterAgent.disabled: it's safe only because - // enabledefault's explicit false wins over this true in the merge (see - // feature/types.go's merge()) when the Cluster Agent is disabled. ManageDependencies - // is a no-op and ManageClusterAgent only writes env vars to a pod template that's - // never deployed, so there's nothing to orphan. return feature.RequiredComponents{ ClusterAgent: feature.RequiredComponent{ IsRequired: new(true), diff --git a/internal/controller/datadogagent/feature/asm/feature_test.go b/internal/controller/datadogagent/feature/asm/feature_test.go index fd4a3e3295..d46accfafa 100644 --- a/internal/controller/datadogagent/feature/asm/feature_test.go +++ b/internal/controller/datadogagent/feature/asm/feature_test.go @@ -68,6 +68,15 @@ func TestASMFeature(t *testing.T) { WantConfigure: true, ClusterAgent: assertEnv(envVar{name: DDAdmissionControllerAppsecEnabled, value: "true", present: true}), }, + { + Name: "ASM Threats enabled but cluster agent disabled", + DDA: testutils.NewDatadogAgentBuilder(). + WithAdmissionControllerEnabled(true). + WithASMEnabled(true, false, false). + WithClusterAgentDisabled(true). + Build(), + WantConfigure: false, + }, { Name: "ASM Threats enabled, admission controller not enabled", DDA: testutils.NewDatadogAgentBuilder(). From 575f88bdeac01629b364a4d77c435cf33a128af3 Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Tue, 6 Oct 2026 15:27:35 +0200 Subject: [PATCH 25/28] fix: don't require the cluster agent for remote config when it is disabled remoteconfig.Configure set ClusterAgent.IsRequired/Containers unconditionally whenever the feature was enabled, regardless of override.clusterAgent.disabled. The Agent requirement stays, since remote config still runs node-side. Co-Authored-By: Claude Sonnet 5 --- .../feature/remoteconfig/feature.go | 17 +++++++----- .../feature/remoteconfig/feature_test.go | 26 +++++++++++++++++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/internal/controller/datadogagent/feature/remoteconfig/feature.go b/internal/controller/datadogagent/feature/remoteconfig/feature.go index 8a00615c87..092377e310 100644 --- a/internal/controller/datadogagent/feature/remoteconfig/feature.go +++ b/internal/controller/datadogagent/feature/remoteconfig/feature.go @@ -64,6 +64,10 @@ func (f *rcFeature) ID() feature.IDType { // Configure is used to configure the feature from a v2alpha1.DatadogAgent instance. func (f *rcFeature) Configure(_ metav1.Object, ddaSpec *v2alpha1.DatadogAgentSpec, _ *v2alpha1.RemoteConfigConfiguration) (reqComp feature.RequiredComponents) { + clusterAgentDisabled := false + if clusterAgent, ok := ddaSpec.Override[v2alpha1.ClusterAgentComponentName]; ok { + clusterAgentDisabled = apiutils.BoolValue(clusterAgent.Disabled) + } if ddaSpec.Features != nil && ddaSpec.Features.RemoteConfiguration != nil && ddaSpec.Features.RemoteConfiguration.Enabled != nil { // If a value exists, explicitly enable or disable Remote Config and override the default @@ -73,17 +77,16 @@ func (f *rcFeature) Configure(_ metav1.Object, ddaSpec *v2alpha1.DatadogAgentSpe // Ref: https://github.com/DataDog/datadog-operator/blob/c4b6e498048a11fbe99d1ea51d2870c6be578799/internal/controller/datadogagent/feature/types.go#L37 if f.enabled { reqComp.Agent.IsRequired = ddaSpec.Features.RemoteConfiguration.Enabled - // Not gated on override.clusterAgent.disabled: the Agent requirement above must - // stay set regardless (remote config still runs node-side), and this is - // otherwise safe only because enabledefault's explicit false wins over this - // true in the merge (see feature/types.go's merge()). ManageDependencies is a - // no-op, so there's nothing to orphan. - reqComp.ClusterAgent.IsRequired = ddaSpec.Features.RemoteConfiguration.Enabled + if !clusterAgentDisabled { + reqComp.ClusterAgent.IsRequired = ddaSpec.Features.RemoteConfiguration.Enabled + } } } reqComp.Agent.Containers = []apicommon.AgentContainerName{apicommon.CoreAgentContainerName} - reqComp.ClusterAgent.Containers = []apicommon.AgentContainerName{apicommon.ClusterAgentContainerName} + if !clusterAgentDisabled { + reqComp.ClusterAgent.Containers = []apicommon.AgentContainerName{apicommon.ClusterAgentContainerName} + } return reqComp } diff --git a/internal/controller/datadogagent/feature/remoteconfig/feature_test.go b/internal/controller/datadogagent/feature/remoteconfig/feature_test.go index 112c54b797..9dc7c084e3 100644 --- a/internal/controller/datadogagent/feature/remoteconfig/feature_test.go +++ b/internal/controller/datadogagent/feature/remoteconfig/feature_test.go @@ -49,11 +49,37 @@ func Test_rcFeature_Configure(t *testing.T) { Agent: rcAgentNodeWantFunc(false), ClusterAgent: rcClusterAgentNodeWantFunc(false), }, + { + Name: "RC enabled but cluster agent disabled", + DDA: testutils.NewDatadogAgentBuilder(). + WithRemoteConfigEnabled(true). + WithClusterAgentDisabled(true). + Build(), + WantConfigure: true, + Agent: rcAgentNodeWantFunc(true), + }, } tests.Run(t, buildRCFeature) } +// Test_rcFeature_Configure_ClusterAgentDisabled verifies that, when the cluster agent is +// disabled via override, Configure does not require it or declare it configured, even though +// remote config is enabled and still runs node-side. +func Test_rcFeature_Configure_ClusterAgentDisabled(t *testing.T) { + dda := testutils.NewDatadogAgentBuilder(). + WithRemoteConfigEnabled(true). + WithClusterAgentDisabled(true). + Build() + + f := buildRCFeature(nil) + reqComp := f.Configure(dda, &dda.Spec, nil) + + assert.False(t, reqComp.ClusterAgent.IsEnabled()) + assert.Empty(t, reqComp.ClusterAgent.Containers) + assert.True(t, reqComp.Agent.IsEnabled()) +} + func rcAgentNodeWantFunc(rcEnabled bool) *test.ComponentTest { return test.NewDefaultComponentTest().WithWantFunc( func(t testing.TB, mgrInterface feature.PodTemplateManagers) { From 40dda0c6aa1cdef3fee948dd971d7944c08d252a Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Tue, 6 Oct 2026 15:27:40 +0200 Subject: [PATCH 26/28] fix: don't require the cluster agent for Prometheus scrape when it is disabled prometheusscrape.Configure set ClusterAgent.IsRequired unconditionally whenever the feature was enabled, regardless of override.clusterAgent.disabled. The Agent requirement stays, since Prometheus scrape still runs node-side. Co-Authored-By: Claude Sonnet 5 --- .../feature/prometheusscrape/feature.go | 17 ++++---- .../feature/prometheusscrape/feature_test.go | 40 +++++++++++++++++++ 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/internal/controller/datadogagent/feature/prometheusscrape/feature.go b/internal/controller/datadogagent/feature/prometheusscrape/feature.go index f74f7a4005..0b495b09cd 100644 --- a/internal/controller/datadogagent/feature/prometheusscrape/feature.go +++ b/internal/controller/datadogagent/feature/prometheusscrape/feature.go @@ -64,18 +64,19 @@ func (f *prometheusScrapeFeature) Configure(_ metav1.Object, ddaSpec *v2alpha1.D apicommon.CoreAgentContainerName, }, }, - // Not gated on override.clusterAgent.disabled: the Agent requirement above must - // stay set regardless (Prometheus scrape still runs node-side), and this is - // otherwise safe only because enabledefault's explicit false wins over this - // true in the merge (see feature/types.go's merge()). ManageDependencies is a - // no-op and ManageClusterAgent only writes an env var to a pod template that's - // never deployed, so there's nothing to orphan. - ClusterAgent: feature.RequiredComponent{ + } + + clusterAgentDisabled := false + if clusterAgent, ok := ddaSpec.Override[v2alpha1.ClusterAgentComponentName]; ok { + clusterAgentDisabled = apiutils.BoolValue(clusterAgent.Disabled) + } + if !clusterAgentDisabled { + reqComp.ClusterAgent = feature.RequiredComponent{ IsRequired: new(true), Containers: []apicommon.AgentContainerName{ apicommon.ClusterAgentContainerName, }, - }, + } } } diff --git a/internal/controller/datadogagent/feature/prometheusscrape/feature_test.go b/internal/controller/datadogagent/feature/prometheusscrape/feature_test.go index 694ba74116..8faca6faf8 100644 --- a/internal/controller/datadogagent/feature/prometheusscrape/feature_test.go +++ b/internal/controller/datadogagent/feature/prometheusscrape/feature_test.go @@ -211,11 +211,51 @@ func Test_prometheusScrapeFeature_Configure(t *testing.T) { }, ), }, + { + Name: "Prometheus scrape enabled but cluster agent disabled", + DDA: testutils.NewDatadogAgentBuilder(). + WithPrometheusScrapeEnabled(true). + WithClusterAgentDisabled(true). + Build(), + WantConfigure: true, + Agent: test.NewDefaultComponentTest().WithWantFunc( + func(t testing.TB, mgrInterface feature.PodTemplateManagers) { + wantEnvVars := []*corev1.EnvVar{ + { + Name: DDPrometheusScrapeEnabled, + Value: "true", + }, + { + Name: DDPrometheusScrapeServiceEndpoints, + Value: "false", + }, + } + assertContainerEnvVars(t, mgrInterface, apicommon.CoreAgentContainerName, wantEnvVars) + }, + ), + }, } tests.Run(t, buildPrometheusScrapeFeature) } +// Test_prometheusScrapeFeature_Configure_ClusterAgentDisabled verifies that, when the cluster +// agent is disabled via override, Configure does not require it or declare it configured, even +// though Prometheus scrape is enabled and still runs node-side. +func Test_prometheusScrapeFeature_Configure_ClusterAgentDisabled(t *testing.T) { + dda := testutils.NewDatadogAgentBuilder(). + WithPrometheusScrapeEnabled(true). + WithClusterAgentDisabled(true). + Build() + + f := buildPrometheusScrapeFeature(nil) + reqComp := f.Configure(dda, &dda.Spec, nil) + + assert.False(t, reqComp.ClusterAgent.IsEnabled()) + assert.Empty(t, reqComp.ClusterAgent.Containers) + assert.True(t, reqComp.Agent.IsEnabled()) +} + func assertContainerEnvVars(t testing.TB, mgrInterface feature.PodTemplateManagers, containerName apicommon.AgentContainerName, wantEnvVars []*corev1.EnvVar) { mgr := mgrInterface.(*fake.PodTemplateManagers) envVars := mgr.EnvVarMgr.EnvVarsByC[containerName] From 57e8992e73d22ccd2063c7b99b221dd0e102c3b2 Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Tue, 6 Oct 2026 15:27:46 +0200 Subject: [PATCH 27/28] docs: update merge() comment now that asm/prometheusscrape/remoteconfig are gated The merge-priority safety net was documented as something those three features relied on instead of checking the override themselves. Now that they do, reframe the comment as a safety net for future features, not a substitute for the per-feature check. Co-Authored-By: Claude Sonnet 5 --- internal/controller/datadogagent/feature/types.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/controller/datadogagent/feature/types.go b/internal/controller/datadogagent/feature/types.go index 316fe268f7..b636693ada 100644 --- a/internal/controller/datadogagent/feature/types.go +++ b/internal/controller/datadogagent/feature/types.go @@ -103,10 +103,11 @@ func (rc *RequiredComponent) Merge(in *RequiredComponent) *RequiredComponent { // // This is relied upon for the no-Cluster-Agent configuration (override.clusterAgent.disabled): // the enabledefault feature resolves its ClusterAgent.IsRequired to an explicit false in that -// case, which wins here over any other feature that still unconditionally requires the Cluster -// Agent (e.g. asm, prometheusscrape, remoteconfig), keeping the merged result correctly -// "not required" even for features that don't individually check the override. Those features -// are only safe because of this merge order — if it changes, re-audit them. +// case, which wins here over any other feature's true. Every feature that can require the +// Cluster Agent is still expected to check the override itself in Configure() and skip that +// requirement when disabled — this merge priority is a safety net, not a substitute for that +// check, so a feature whose ManageDependencies creates real Cluster-Agent-only resources must +// not rely on it alone. func merge(a, b *bool) *bool { if a == nil && b == nil { return nil From 4d3b88ea1f0f5df8c2ca7f1a9d722286d8c48520 Mon Sep 17 00:00:00 2001 From: Antoine GAGNIERE Date: Tue, 6 Oct 2026 18:28:35 +0200 Subject: [PATCH 28/28] test(apm): lock in SSI being skipped when the cluster agent is disabled A Copilot review comment questioned whether APM's single-step instrumentation (language detection in particular) gets left half configured when override.clusterAgent.disabled is set. Verified it doesn't: shouldConfigureSSI already calls validateSSISharedComponentPrerequisites, which rejects SSI when the Cluster Agent is disabled, so singleStepInstrumentation stays nil and shouldEnableLanguageDetection() is false. No production code change; adding a regression test since nothing previously covered this case. Co-Authored-By: Claude Sonnet 5 --- .../datadogagent/feature/apm/feature_test.go | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/internal/controller/datadogagent/feature/apm/feature_test.go b/internal/controller/datadogagent/feature/apm/feature_test.go index 198d3f095c..d22b515b77 100644 --- a/internal/controller/datadogagent/feature/apm/feature_test.go +++ b/internal/controller/datadogagent/feature/apm/feature_test.go @@ -106,6 +106,43 @@ func TestAPMConfigureSSIWithoutNodeAPM(t *testing.T) { assert.True(t, feat.shouldEnableLanguageDetection()) } +// TestAPMConfigureSSIClusterAgentDisabled verifies that SSI (including language detection, +// which runs on the Cluster Agent admission controller path) is not configured when the +// Cluster Agent is disabled via override, even though admissionController and SSI are both +// otherwise enabled. This is handled by validateSSISharedComponentPrerequisites (checked via +// shouldConfigureSSI), not by a dedicated override check in this feature. +func TestAPMConfigureSSIClusterAgentDisabled(t *testing.T) { + dda := &v2alpha1.DatadogAgent{ + ObjectMeta: v1.ObjectMeta{ + Name: "datadog", + Namespace: "default", + }, + Spec: v2alpha1.DatadogAgentSpec{ + Features: &v2alpha1.DatadogFeatures{ + AdmissionController: &v2alpha1.AdmissionControllerFeatureConfig{Enabled: ptr.To(true)}, + APM: &v2alpha1.APMFeatureConfig{ + Enabled: ptr.To(false), + SingleStepInstrumentation: &v2alpha1.SingleStepInstrumentation{ + Enabled: ptr.To(true), + LanguageDetection: &v2alpha1.LanguageDetectionConfig{Enabled: ptr.To(true)}, + }, + }, + }, + Override: map[v2alpha1.ComponentName]*v2alpha1.DatadogAgentComponentOverride{ + v2alpha1.ClusterAgentComponentName: { + Disabled: ptr.To(true), + }, + }, + }, + } + + feat := buildAPMFeature(&feature.Options{}).(*apmFeature) + reqComp := feat.Configure(dda, &dda.Spec, nil) + + assert.False(t, reqComp.ClusterAgent.IsEnabled()) + assert.False(t, feat.shouldEnableLanguageDetection()) +} + func TestAPMFeature(t *testing.T) { tests := test.FeatureTestSuite{ {