Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8713b5f
fix: explicitly propagate DD_DISCOVERY_ENABLED=false when service dis…
agagniere Oct 2, 2026
012a73c
fix: don't enable instrumentationCrd when the cluster agent is disabled
agagniere Oct 2, 2026
6f32dce
fix: don't enable kubeStateMetricsCore when the cluster agent is disa…
agagniere Oct 2, 2026
645e587
fix: don't create orchestratorExplorer RBAC when the cluster agent is…
agagniere Oct 2, 2026
6295056
fix: don't enable eventCollection when the cluster agent is disabled
agagniere Oct 2, 2026
e854f59
fix: don't enable admissionController when the cluster agent is disabled
agagniere Oct 2, 2026
205431c
fix: don't enable appsec injector when the cluster agent is disabled
agagniere Oct 2, 2026
1b49e45
fix: don't enable CSPM when the cluster agent is disabled
agagniere Oct 2, 2026
aa607c0
fix: don't enable externalMetricsServer when the cluster agent is dis…
agagniere Oct 2, 2026
de38866
fix: don't enable helmCheck when the cluster agent is disabled
agagniere Oct 2, 2026
361c0b9
fix: don't enable kubernetesActions when the cluster agent is disabled
agagniere Oct 2, 2026
707fba3
fix: don't enable autoscaling when the cluster agent is disabled
agagniere Oct 2, 2026
ef12837
fix: don't enable controlPlaneMonitoring when the cluster agent is di…
agagniere Oct 2, 2026
9a8cdd8
fix: reflect override.clusterAgent.disabled in DD_CLUSTER_AGENT_ENABLED
agagniere Oct 2, 2026
d00766c
fix(servicediscovery): write resolved enabled state on single-contain…
agagniere Oct 5, 2026
413800d
test(renderer): add golden fixture for the no-cluster-agent scenario
agagniere Oct 5, 2026
a46d55b
docs(cluster-agent): document running without the Cluster Agent
agagniere Oct 5, 2026
6f4bf0a
fix: don't create DCA token Secret or Service when cluster agent is d…
agagniere Oct 5, 2026
5d91599
fix: don't write DD_CLUSTER_AGENT_AUTH_TOKEN when cluster agent is di…
agagniere Oct 5, 2026
4d4f619
fix: don't flag no-cluster-agent as an override conflict
agagniere Oct 5, 2026
693ae2a
fix: don't enable clusterChecks when the cluster agent is disabled
agagniere Oct 5, 2026
4d1fd45
fix: don't enable cluster-side private action runner when the cluster…
agagniere Oct 6, 2026
6849a21
docs: flag remaining unguarded ClusterAgent.IsRequired as fragile, no…
agagniere Oct 6, 2026
7207e97
fix: don't require the cluster agent for ASM when it is disabled
agagniere Oct 6, 2026
575f88b
fix: don't require the cluster agent for remote config when it is dis…
agagniere Oct 6, 2026
40dda0c
fix: don't require the cluster agent for Prometheus scrape when it is…
agagniere Oct 6, 2026
57e8992
docs: update merge() comment now that asm/prometheusscrape/remoteconf…
agagniere Oct 6, 2026
4d3b88e
test(apm): lock in SSI being skipped when the cluster agent is disabled
agagniere Oct 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/cluster_agent_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<DATADOG_API_KEY>"

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.
Comment thread
agagniere marked this conversation as resolved.

[2]: https://github.com/DataDog/datadog-operator/blob/main/examples/datadogagent/datadog-agent-without-clusteragent.yaml
13 changes: 13 additions & 0 deletions examples/datadogagent/datadog-agent-without-clusteragent.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: datadoghq.com/v2alpha1
kind: DatadogAgent
metadata:
name: datadog
spec:
global:
clusterName: my-example-cluster
credentials:
apiKey: <DATADOG_API_KEY>

override:
clusterAgent:
disabled: true
29 changes: 19 additions & 10 deletions internal/controller/datadogagent/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -55,24 +56,32 @@ 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.
if err := global.AddAPMTelemetryDependencies(logger, instance, resourceManagers); err != nil {
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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().
Expand Down
37 changes: 37 additions & 0 deletions internal/controller/datadogagent/feature/apm/feature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
{
Expand Down
6 changes: 6 additions & 0 deletions internal/controller/datadogagent/feature/appsec/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions internal/controller/datadogagent/feature/appsec/feature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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().
Expand Down
4 changes: 4 additions & 0 deletions internal/controller/datadogagent/feature/asm/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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().
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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().
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
9 changes: 8 additions & 1 deletion internal/controller/datadogagent/feature/cspm/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
11 changes: 11 additions & 0 deletions internal/controller/datadogagent/feature/cspm/feature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading