diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index a7836f21f2..a35e67c915 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -188,6 +188,7 @@ rules: - create - get - list + - patch - watch - apiGroups: - autoscaling diff --git a/internal/controller/datadogagent/common/volumes.go b/internal/controller/datadogagent/common/volumes.go index 195dcb58b3..2c88d10bb1 100644 --- a/internal/controller/datadogagent/common/volumes.go +++ b/internal/controller/datadogagent/common/volumes.go @@ -295,6 +295,7 @@ func GetVolumeMountForSecurity() corev1.VolumeMount { return corev1.VolumeMount{ Name: SeccompSecurityVolumeName, MountPath: SeccompSecurityVolumePath, + ReadOnly: true, } } diff --git a/internal/controller/datadogagent/component/agent/rbac.go b/internal/controller/datadogagent/component/agent/rbac.go index ac1ce6f2ac..702898c5af 100644 --- a/internal/controller/datadogagent/component/agent/rbac.go +++ b/internal/controller/datadogagent/component/agent/rbac.go @@ -14,8 +14,10 @@ import ( // RBAC for Agent -// GetDefaultAgentClusterRolePolicyRules returns the default policy rules for the Agent cluster role -func GetDefaultAgentClusterRolePolicyRules(excludeNonResourceRules bool, useFineGrainedAuthorization bool) []rbacv1.PolicyRule { +// GetDefaultAgentClusterRolePolicyRules returns the default policy rules for the Agent cluster role. +// kubeletUseAPIServer adds get/list on pods so the Agent can discover pods via +// the API server when the kubelet endpoint is not reachable (e.g. GKE Autopilot). +func GetDefaultAgentClusterRolePolicyRules(excludeNonResourceRules bool, useFineGrainedAuthorization bool, kubeletUseAPIServer bool) []rbacv1.PolicyRule { policyRule := []rbacv1.PolicyRule{ getKubeletPolicyRule(useFineGrainedAuthorization), getEndpointsPolicyRule(), @@ -23,6 +25,10 @@ func GetDefaultAgentClusterRolePolicyRules(excludeNonResourceRules bool, useFine component.GetEKSControlPlaneMetricsPolicyRule(), } + if kubeletUseAPIServer { + policyRule = append(policyRule, getPodsPolicyRule()) + } + if !excludeNonResourceRules { policyRule = append(policyRule, getMetricsEndpointPolicyRule()) } @@ -30,6 +36,14 @@ func GetDefaultAgentClusterRolePolicyRules(excludeNonResourceRules bool, useFine return policyRule } +func getPodsPolicyRule() rbacv1.PolicyRule { + return rbacv1.PolicyRule{ + APIGroups: []string{rbac.CoreAPIGroup}, + Resources: []string{rbac.PodsResource}, + Verbs: []string{rbac.GetVerb, rbac.ListVerb}, + } +} + func getMetricsEndpointPolicyRule() rbacv1.PolicyRule { return rbacv1.PolicyRule{ NonResourceURLs: []string{ diff --git a/internal/controller/datadogagent/component/agent/rbac_test.go b/internal/controller/datadogagent/component/agent/rbac_test.go new file mode 100644 index 0000000000..cc9ce68e97 --- /dev/null +++ b/internal/controller/datadogagent/component/agent/rbac_test.go @@ -0,0 +1,45 @@ +// 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 2025-present Datadog, Inc. + +package agent + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/DataDog/datadog-operator/pkg/kubernetes/rbac" +) + +func TestGetDefaultAgentClusterRolePolicyRules_KubeletUseAPIServer(t *testing.T) { + tests := []struct { + name string + kubeletUseAPIServer bool + expectPodsRule bool + }{ + {name: "no kubelet api server -> no pods rule", kubeletUseAPIServer: false, expectPodsRule: false}, + {name: "kubelet api server -> pods get/list rule", kubeletUseAPIServer: true, expectPodsRule: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rules := GetDefaultAgentClusterRolePolicyRules(false, false, tt.kubeletUseAPIServer) + + found := false + for _, r := range rules { + for _, res := range r.Resources { + if res != rbac.PodsResource { + continue + } + found = true + assert.ElementsMatch(t, []string{rbac.GetVerb, rbac.ListVerb}, r.Verbs, + "pods rule should grant get and list") + assert.Equal(t, []string{rbac.CoreAPIGroup}, r.APIGroups) + } + } + assert.Equal(t, tt.expectPodsRule, found) + }) + } +} diff --git a/internal/controller/datadogagent/controller_v2_test.go b/internal/controller/datadogagent/controller_v2_test.go index 4941b0359d..7bb8b6cea7 100644 --- a/internal/controller/datadogagent/controller_v2_test.go +++ b/internal/controller/datadogagent/controller_v2_test.go @@ -1248,6 +1248,9 @@ func Test_AutopilotOverrides(t *testing.T) { ds := &appsv1.DaemonSet{} err := c.Get(context.TODO(), types.NamespacedName{Namespace: resourcesNamespace, Name: dsName}, ds) assert.NoError(t, err, "Failed to get DaemonSet %s/%s", resourcesNamespace, dsName) + assertNoDanglingVolumeMounts(t, ds.Spec.Template.Spec) + assertNoEnvVarInPodSpec(t, ds.Spec.Template.Spec, common.DDAuthTokenFilePath) + assertSeccompSecurityMountReadOnly(t, ds.Spec.Template.Spec) forbiddenVolumes := map[string]struct{}{ common.AuthVolumeName: {}, @@ -1334,6 +1337,9 @@ func Test_AutopilotOverrides(t *testing.T) { ds := &appsv1.DaemonSet{} err := c.Get(context.TODO(), types.NamespacedName{Namespace: resourcesNamespace, Name: dsName}, ds) assert.NoError(t, err, "Failed to get DaemonSet %s/%s", resourcesNamespace, dsName) + assertNoDanglingVolumeMounts(t, ds.Spec.Template.Spec) + assertNoEnvVarInPodSpec(t, ds.Spec.Template.Spec, common.DDAuthTokenFilePath) + assertSeccompSecurityMountReadOnly(t, ds.Spec.Template.Spec) traceAgentFound := false for _, ctn := range ds.Spec.Template.Spec.Containers { @@ -1389,6 +1395,59 @@ func verifyDaemonsetContainers(t *testing.T, c client.Client, resourcesNamespace assert.Equal(t, expectedContainers, dsContainers, "Container names don't match") } +func assertNoDanglingVolumeMounts(t *testing.T, podSpec corev1.PodSpec) { + t.Helper() + + volumes := map[string]struct{}{} + for _, volume := range podSpec.Volumes { + volumes[volume.Name] = struct{}{} + } + + for _, container := range podSpec.InitContainers { + for _, mount := range container.VolumeMounts { + _, found := volumes[mount.Name] + assert.True(t, found, "init container %s has mount %s without a matching volume", container.Name, mount.Name) + } + } + for _, container := range podSpec.Containers { + for _, mount := range container.VolumeMounts { + _, found := volumes[mount.Name] + assert.True(t, found, "container %s has mount %s without a matching volume", container.Name, mount.Name) + } + } +} + +func assertNoEnvVarInPodSpec(t *testing.T, podSpec corev1.PodSpec, name string) { + t.Helper() + + for _, container := range podSpec.InitContainers { + for _, env := range container.Env { + assert.NotEqual(t, name, env.Name, "init container %s should not have env var %s", container.Name, name) + } + } + for _, container := range podSpec.Containers { + for _, env := range container.Env { + assert.NotEqual(t, name, env.Name, "container %s should not have env var %s", container.Name, name) + } + } +} + +func assertSeccompSecurityMountReadOnly(t *testing.T, podSpec corev1.PodSpec) { + t.Helper() + + for _, container := range podSpec.InitContainers { + if container.Name != string(apicommon.SeccompSetupContainerName) { + continue + } + for _, mount := range container.VolumeMounts { + if mount.Name == common.SeccompSecurityVolumeName { + assert.True(t, mount.ReadOnly, "init container %s should mount %s read-only", container.Name, mount.Name) + return + } + } + } +} + func verifyPDB(t *testing.T, c client.Client) { pdbList := policyv1.PodDisruptionBudgetList{} err := c.List(context.TODO(), &pdbList) diff --git a/internal/controller/datadogagent/experimental/autopilot.go b/internal/controller/datadogagent/experimental/autopilot.go index 8d62dd4a40..f5f2e70fd2 100644 --- a/internal/controller/datadogagent/experimental/autopilot.go +++ b/internal/controller/datadogagent/experimental/autopilot.go @@ -14,9 +14,20 @@ import ( 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" + "github.com/DataDog/datadog-operator/internal/controller/datadogagent/object" "github.com/DataDog/datadog-operator/pkg/allowlistsynchronizer" ) +// DDKubeletUseAPIServer is the env var that toggles the Agent's use of the +// Kubernetes API server (instead of the kubelet) to discover pods. It is +// required on GKE Autopilot, where the kubelet endpoint is not reachable. +const DDKubeletUseAPIServer = "DD_KUBELET_USE_API_SERVER" + +// DDCloudProviderMetadata restricts host alias collection to GCP metadata. +const DDCloudProviderMetadata = "DD_CLOUD_PROVIDER_METADATA" + +const autopilotLogCollectionStoragePath = "/var/autopilot/addon/datadog/logs" + var ( forbiddenAgentVolumes = map[string]struct{}{ common.AuthVolumeName: {}, @@ -36,6 +47,12 @@ var ( common.DogstatsdSocketVolumeName: {}, } + forbiddenUnprivilegedSingleAgentMounts = map[string]struct{}{ + common.AuthVolumeName: {}, + common.CriSocketVolumeName: {}, + common.DogstatsdSocketVolumeName: {}, + } + forbiddenTraceAgentMounts = map[string]struct{}{ common.AuthVolumeName: {}, common.CriSocketVolumeName: {}, @@ -50,6 +67,32 @@ var ( common.CriSocketVolumeName: {}, common.DogstatsdSocketVolumeName: {}, } + + forbiddenSystemProbeMounts = map[string]struct{}{ + common.AuthVolumeName: {}, + common.CriSocketVolumeName: {}, + common.DogstatsdSocketVolumeName: {}, + } + + forbiddenSecurityAgentMounts = map[string]struct{}{ + common.AuthVolumeName: {}, + } + + forbiddenOtelAgentMounts = map[string]struct{}{ + common.AuthVolumeName: {}, + } + + forbiddenHostProfilerMounts = map[string]struct{}{ + common.AuthVolumeName: {}, + } + + forbiddenAgentDataPlaneMounts = map[string]struct{}{ + common.AuthVolumeName: {}, + } + + forbiddenPrivateActionRunnerMounts = map[string]struct{}{ + common.AuthVolumeName: {}, + } ) func IsAutopilotEnabled(obj metav1.Object) bool { @@ -66,7 +109,21 @@ func IsAutopilotEnabled(obj metav1.Object) bool { func applyExperimentalAutopilotOverrides(dda metav1.Object, manager feature.PodTemplateManagers) { if IsAutopilotEnabled(dda) { - allowlistsynchronizer.CreateAllowlistSynchronizer() + allowlistsynchronizer.CreateAllowlistSynchronizer( + getExperimentalAnnotation(dda, ExperimentalAutopilotAllowlistVersionSubkey), + object.NewPartOfLabelValue(dda).String(), + ) + + // On Autopilot the kubelet endpoint is not reachable, so the Agent must + // use the API server to discover pods. + manager.EnvVar().AddEnvVar(&corev1.EnvVar{ + Name: DDKubeletUseAPIServer, + Value: "true", + }) + manager.EnvVar().AddEnvVar(&corev1.EnvVar{ + Name: DDCloudProviderMetadata, + Value: `["gcp"]`, + }) if manager.PodTemplateSpec().Labels == nil { manager.PodTemplateSpec().Labels = map[string]string{} @@ -85,15 +142,43 @@ func applyExperimentalAutopilotOverrides(dda metav1.Object, manager feature.PodT v := manager.PodTemplateSpec().Spec.Volumes[:0] for _, vol := range manager.PodTemplateSpec().Spec.Volumes { if _, found := forbiddenAgentVolumes[vol.Name]; !found { + // The GKE Autopilot WorkloadAllowlist only permits this hostPath for log tailing metadata. + if vol.Name == common.RunPathVolumeName && vol.HostPath != nil { + vol.HostPath.Path = autopilotLogCollectionStoragePath + } v = append(v, vol) } } manager.PodTemplateSpec().Spec.Volumes = v + // Remove auth token file path env var + for idx := range manager.PodTemplateSpec().Spec.InitContainers { + env := []corev1.EnvVar{} + for _, e := range manager.PodTemplateSpec().Spec.InitContainers[idx].Env { + if e.Name != common.DDAuthTokenFilePath { + env = append(env, e) + } + } + manager.PodTemplateSpec().Spec.InitContainers[idx].Env = env + } + + for idx := range manager.PodTemplateSpec().Spec.Containers { + env := []corev1.EnvVar{} + for _, e := range manager.PodTemplateSpec().Spec.Containers[idx].Env { + if e.Name != common.DDAuthTokenFilePath { + env = append(env, e) + } + } + manager.PodTemplateSpec().Spec.Containers[idx].Env = env + } + // Remove init-container volume mounts for idx := range manager.PodTemplateSpec().Spec.InitContainers { vm := []corev1.VolumeMount{} for _, m := range manager.PodTemplateSpec().Spec.InitContainers[idx].VolumeMounts { + if m.Name == common.SeccompSecurityVolumeName { + m.ReadOnly = true + } if _, found := forbiddenInitMounts[m.Name]; !found { vm = append(vm, m) } @@ -114,6 +199,19 @@ func applyExperimentalAutopilotOverrides(dda metav1.Object, manager feature.PodT } } + // Remove unprivileged single agent container volume mounts + for idx := range manager.PodTemplateSpec().Spec.Containers { + if manager.PodTemplateSpec().Spec.Containers[idx].Name == string(apicommon.UnprivilegedSingleAgentContainerName) { + vm := []corev1.VolumeMount{} + for _, m := range manager.PodTemplateSpec().Spec.Containers[idx].VolumeMounts { + if _, found := forbiddenUnprivilegedSingleAgentMounts[m.Name]; !found { + vm = append(vm, m) + } + } + manager.PodTemplateSpec().Spec.Containers[idx].VolumeMounts = vm + } + } + // Remove trace agent container volume mounts and change command for idx := range manager.PodTemplateSpec().Spec.Containers { if manager.PodTemplateSpec().Spec.Containers[idx].Name == string(apicommon.TraceAgentContainerName) { @@ -149,5 +247,83 @@ func applyExperimentalAutopilotOverrides(dda metav1.Object, manager feature.PodT } } } + + // Remove system-probe container volume mounts + for idx := range manager.PodTemplateSpec().Spec.Containers { + if manager.PodTemplateSpec().Spec.Containers[idx].Name == string(apicommon.SystemProbeContainerName) { + vm := []corev1.VolumeMount{} + for _, m := range manager.PodTemplateSpec().Spec.Containers[idx].VolumeMounts { + if _, found := forbiddenSystemProbeMounts[m.Name]; !found { + vm = append(vm, m) + } + } + manager.PodTemplateSpec().Spec.Containers[idx].VolumeMounts = vm + } + } + + // Remove security agent container volume mounts + for idx := range manager.PodTemplateSpec().Spec.Containers { + if manager.PodTemplateSpec().Spec.Containers[idx].Name == string(apicommon.SecurityAgentContainerName) { + vm := []corev1.VolumeMount{} + for _, m := range manager.PodTemplateSpec().Spec.Containers[idx].VolumeMounts { + if _, found := forbiddenSecurityAgentMounts[m.Name]; !found { + vm = append(vm, m) + } + } + manager.PodTemplateSpec().Spec.Containers[idx].VolumeMounts = vm + } + } + + // Remove otel agent container volume mounts + for idx := range manager.PodTemplateSpec().Spec.Containers { + if manager.PodTemplateSpec().Spec.Containers[idx].Name == string(apicommon.OtelAgent) { + vm := []corev1.VolumeMount{} + for _, m := range manager.PodTemplateSpec().Spec.Containers[idx].VolumeMounts { + if _, found := forbiddenOtelAgentMounts[m.Name]; !found { + vm = append(vm, m) + } + } + manager.PodTemplateSpec().Spec.Containers[idx].VolumeMounts = vm + } + } + + // Remove host profiler container volume mounts + for idx := range manager.PodTemplateSpec().Spec.Containers { + if manager.PodTemplateSpec().Spec.Containers[idx].Name == string(apicommon.HostProfiler) { + vm := []corev1.VolumeMount{} + for _, m := range manager.PodTemplateSpec().Spec.Containers[idx].VolumeMounts { + if _, found := forbiddenHostProfilerMounts[m.Name]; !found { + vm = append(vm, m) + } + } + manager.PodTemplateSpec().Spec.Containers[idx].VolumeMounts = vm + } + } + + // Remove agent data plane container volume mounts + for idx := range manager.PodTemplateSpec().Spec.Containers { + if manager.PodTemplateSpec().Spec.Containers[idx].Name == string(apicommon.AgentDataPlaneContainerName) { + vm := []corev1.VolumeMount{} + for _, m := range manager.PodTemplateSpec().Spec.Containers[idx].VolumeMounts { + if _, found := forbiddenAgentDataPlaneMounts[m.Name]; !found { + vm = append(vm, m) + } + } + manager.PodTemplateSpec().Spec.Containers[idx].VolumeMounts = vm + } + } + + // Remove private action runner container volume mounts + for idx := range manager.PodTemplateSpec().Spec.Containers { + if manager.PodTemplateSpec().Spec.Containers[idx].Name == string(apicommon.PrivateActionRunnerContainerName) { + vm := []corev1.VolumeMount{} + for _, m := range manager.PodTemplateSpec().Spec.Containers[idx].VolumeMounts { + if _, found := forbiddenPrivateActionRunnerMounts[m.Name]; !found { + vm = append(vm, m) + } + } + manager.PodTemplateSpec().Spec.Containers[idx].VolumeMounts = vm + } + } } } diff --git a/internal/controller/datadogagent/experimental/autopilot_test.go b/internal/controller/datadogagent/experimental/autopilot_test.go new file mode 100644 index 0000000000..6c25bbbbfe --- /dev/null +++ b/internal/controller/datadogagent/experimental/autopilot_test.go @@ -0,0 +1,409 @@ +// 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 experimental + +import ( + "testing" + + "github.com/stretchr/testify/assert" + v1 "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" + "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" +) + +func findEnvVar(envs []*v1.EnvVar, name string) *v1.EnvVar { + for _, e := range envs { + if e.Name == name { + return e + } + } + return nil +} + +func TestApplyExperimentalAutopilotOverrides_KubeletUseAPIServerEnvVar(t *testing.T) { + tests := []struct { + name string + autopilotEnabled bool + expectEnvVarValue string // empty means env var should NOT be present + }{ + { + name: "autopilot enabled adds DD_KUBELET_USE_API_SERVER=true", + autopilotEnabled: true, + expectEnvVarValue: "true", + }, + { + name: "autopilot disabled does not add the env var", + autopilotEnabled: false, + expectEnvVarValue: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + manager := fake.NewPodTemplateManagers(t, v1.PodTemplateSpec{}) + + dda := &v2alpha1.DatadogAgent{ + ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{}}, + } + if tt.autopilotEnabled { + dda.Annotations[getExperimentalAnnotationKey(ExperimentalAutopilotSubkey)] = "true" + } + + applyExperimentalAutopilotOverrides(dda, manager) + + got := findEnvVar(manager.EnvVarMgr.EnvVarsByC[mergerfake.AllContainers], DDKubeletUseAPIServer) + if tt.expectEnvVarValue == "" { + assert.Nil(t, got, "DD_KUBELET_USE_API_SERVER should not be set when autopilot is disabled") + return + } + if assert.NotNil(t, got, "DD_KUBELET_USE_API_SERVER should be set when autopilot is enabled") { + assert.Equal(t, tt.expectEnvVarValue, got.Value) + } + }) + } +} + +func TestApplyExperimentalAutopilotOverrides_CloudProviderMetadataEnvVar(t *testing.T) { + tests := []struct { + name string + autopilotEnabled bool + expectEnvVarValue string // empty means env var should NOT be present + }{ + { + name: "autopilot enabled restricts cloud provider metadata to GCP", + autopilotEnabled: true, + expectEnvVarValue: `["gcp"]`, + }, + { + name: "autopilot disabled does not set cloud provider metadata", + autopilotEnabled: false, + expectEnvVarValue: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + manager := fake.NewPodTemplateManagers(t, v1.PodTemplateSpec{}) + + dda := &v2alpha1.DatadogAgent{ + ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{}}, + } + if tt.autopilotEnabled { + dda.Annotations[getExperimentalAnnotationKey(ExperimentalAutopilotSubkey)] = "true" + } + + applyExperimentalAutopilotOverrides(dda, manager) + + got := findEnvVar(manager.EnvVarMgr.EnvVarsByC[mergerfake.AllContainers], DDCloudProviderMetadata) + if tt.expectEnvVarValue == "" { + assert.Nil(t, got, "DD_CLOUD_PROVIDER_METADATA should not be set when autopilot is disabled") + return + } + if assert.NotNil(t, got, "DD_CLOUD_PROVIDER_METADATA should be set when autopilot is enabled") { + assert.Equal(t, tt.expectEnvVarValue, got.Value) + } + }) + } +} + +// TestApplyExperimentalAutopilotOverrides_NPMSurvives asserts that the volumes, +// mounts, and HostPID required by the NPM feature on the system-probe container +// are NOT stripped by the Autopilot overrides. NPM on Autopilot relies on the +// WorkloadAllowlist to grant the required exemptions; if the operator strips +// the mounts client-side, the system-probe container will fail to start even +// when the allowlist would have permitted it. +func TestApplyExperimentalAutopilotOverrides_NPMSurvives(t *testing.T) { + npmVolumes := []v1.Volume{ + {Name: common.ProcdirVolumeName}, + {Name: common.CgroupsVolumeName}, + {Name: common.DebugfsVolumeName}, + {Name: common.SystemProbeSocketVolumeName}, + } + npmMounts := []v1.VolumeMount{ + {Name: common.ProcdirVolumeName, MountPath: "/host/proc"}, + {Name: common.CgroupsVolumeName, MountPath: "/host/sys/fs/cgroup"}, + {Name: common.DebugfsVolumeName, MountPath: "/sys/kernel/debug"}, + {Name: common.SystemProbeSocketVolumeName, MountPath: "/var/run/sysprobe"}, + } + + manager := fake.NewPodTemplateManagers(t, v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + HostPID: true, + Volumes: npmVolumes, + Containers: []v1.Container{ + {Name: string(apicommon.SystemProbeContainerName), VolumeMounts: npmMounts}, + {Name: string(apicommon.CoreAgentContainerName)}, + }, + }, + }) + + dda := &v2alpha1.DatadogAgent{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + getExperimentalAnnotationKey(ExperimentalAutopilotSubkey): "true", + }, + }, + } + + applyExperimentalAutopilotOverrides(dda, manager) + + tpl := manager.PodTemplateSpec() + assert.True(t, tpl.Spec.HostPID, "HostPID should be preserved on autopilot for NPM") + + gotVolumes := map[string]bool{} + for _, v := range tpl.Spec.Volumes { + gotVolumes[v.Name] = true + } + for _, want := range npmVolumes { + assert.True(t, gotVolumes[want.Name], "NPM volume %q should survive autopilot overrides", want.Name) + } + + var sysProbeMounts []v1.VolumeMount + for _, c := range tpl.Spec.Containers { + if c.Name == string(apicommon.SystemProbeContainerName) { + sysProbeMounts = c.VolumeMounts + break + } + } + gotMounts := map[string]bool{} + for _, m := range sysProbeMounts { + gotMounts[m.Name] = true + } + for _, want := range npmMounts { + assert.True(t, gotMounts[want.Name], "NPM mount %q should survive autopilot overrides on system-probe", want.Name) + } +} + +func TestApplyExperimentalAutopilotOverrides_LogCollectionStoragePath(t *testing.T) { + tests := []struct { + name string + autopilotEnabled bool + inputPath string + wantPath string + }{ + { + name: "autopilot enabled rewrites log collection storage hostPath", + autopilotEnabled: true, + inputPath: common.DefaultLogTempStoragePath, + wantPath: autopilotLogCollectionStoragePath, + }, + { + name: "autopilot disabled preserves log collection storage hostPath", + autopilotEnabled: false, + inputPath: common.DefaultLogTempStoragePath, + wantPath: common.DefaultLogTempStoragePath, + }, + { + name: "autopilot enabled rewrites custom log collection storage hostPath", + autopilotEnabled: true, + inputPath: "/custom/log/storage", + wantPath: autopilotLogCollectionStoragePath, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + manager := fake.NewPodTemplateManagers(t, v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + Volumes: []v1.Volume{ + { + Name: common.RunPathVolumeName, + VolumeSource: v1.VolumeSource{ + HostPath: &v1.HostPathVolumeSource{ + Path: tt.inputPath, + }, + }, + }, + }, + }, + }) + + dda := &v2alpha1.DatadogAgent{ + ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{}}, + } + if tt.autopilotEnabled { + dda.Annotations[getExperimentalAnnotationKey(ExperimentalAutopilotSubkey)] = "true" + } + + applyExperimentalAutopilotOverrides(dda, manager) + + volumes := manager.PodTemplateSpec().Spec.Volumes + if assert.Len(t, volumes, 1) && assert.NotNil(t, volumes[0].HostPath) { + assert.Equal(t, tt.wantPath, volumes[0].HostPath.Path) + } + }) + } +} + +func TestApplyExperimentalAutopilotOverrides_RunPathEmptyDirIsPreserved(t *testing.T) { + manager := fake.NewPodTemplateManagers(t, v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + Volumes: []v1.Volume{ + common.GetVolumeForRunPath(), + }, + }, + }) + dda := &v2alpha1.DatadogAgent{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + getExperimentalAnnotationKey(ExperimentalAutopilotSubkey): "true", + }, + }, + } + + applyExperimentalAutopilotOverrides(dda, manager) + + volumes := manager.PodTemplateSpec().Spec.Volumes + if assert.Len(t, volumes, 1) { + assert.Nil(t, volumes[0].HostPath) + assert.NotNil(t, volumes[0].EmptyDir) + } +} + +func TestApplyExperimentalAutopilotOverrides_RemovesAuthTokenFilePathAndAuthMounts(t *testing.T) { + authEnv := []v1.EnvVar{ + {Name: common.DDAuthTokenFilePath, Value: "/etc/datadog-agent/auth/token"}, + {Name: common.DDClusterAgentEnabled, Value: "true"}, + } + + manager := fake.NewPodTemplateManagers(t, v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + InitContainers: []v1.Container{ + { + Name: string(apicommon.InitConfigContainerName), + Env: authEnv, + VolumeMounts: []v1.VolumeMount{ + {Name: common.AuthVolumeName}, + {Name: common.CriSocketVolumeName}, + {Name: common.LogDatadogVolumeName}, + }, + }, + { + Name: string(apicommon.SeccompSetupContainerName), + VolumeMounts: []v1.VolumeMount{ + {Name: common.SeccompSecurityVolumeName, ReadOnly: false}, + {Name: common.SeccompRootVolumeName, ReadOnly: false}, + }, + }, + }, + Volumes: []v1.Volume{ + {Name: common.LogDatadogVolumeName}, + {Name: common.AuthVolumeName}, + {Name: common.DogstatsdSocketVolumeName}, + {Name: common.SeccompSecurityVolumeName}, + {Name: common.SeccompRootVolumeName}, + {Name: common.ProcdirVolumeName}, + {Name: common.CgroupsVolumeName}, + }, + Containers: []v1.Container{ + { + Name: string(apicommon.SystemProbeContainerName), + Env: authEnv, + VolumeMounts: []v1.VolumeMount{ + {Name: common.LogDatadogVolumeName}, + {Name: common.AuthVolumeName}, + {Name: common.DogstatsdSocketVolumeName}, + {Name: common.ProcdirVolumeName}, + {Name: common.CgroupsVolumeName}, + }, + }, + { + Name: string(apicommon.OtelAgent), + Env: authEnv, + VolumeMounts: []v1.VolumeMount{ + {Name: common.AuthVolumeName}, + {Name: common.LogDatadogVolumeName}, + }, + }, + { + Name: string(apicommon.AgentDataPlaneContainerName), + Env: authEnv, + VolumeMounts: []v1.VolumeMount{ + {Name: common.AuthVolumeName}, + {Name: common.LogDatadogVolumeName}, + }, + }, + }, + }, + }) + + dda := &v2alpha1.DatadogAgent{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + getExperimentalAnnotationKey(ExperimentalAutopilotSubkey): "true", + }, + }, + } + + applyExperimentalAutopilotOverrides(dda, manager) + + tpl := manager.PodTemplateSpec() + remainingVolumes := map[string]bool{} + for _, v := range tpl.Spec.Volumes { + remainingVolumes[v.Name] = true + } + + assert.False(t, remainingVolumes[common.AuthVolumeName], "auth volume should be stripped on autopilot") + assert.False(t, remainingVolumes[common.DogstatsdSocketVolumeName], "DogStatsD socket volume should be stripped on autopilot") + + for _, c := range tpl.Spec.InitContainers { + for _, e := range c.Env { + assert.NotEqual(t, common.DDAuthTokenFilePath, e.Name, "init container %s should not keep DD_AUTH_TOKEN_FILE_PATH on autopilot", c.Name) + } + for _, m := range c.VolumeMounts { + assert.NotEqual(t, common.AuthVolumeName, m.Name, "init container %s should not keep auth mount on autopilot", c.Name) + if c.Name == string(apicommon.SeccompSetupContainerName) && m.Name == common.SeccompSecurityVolumeName { + assert.True(t, m.ReadOnly, "seccomp-setup datadog-agent-security mount should be read-only on autopilot") + } + } + } + + for _, c := range tpl.Spec.Containers { + mounts := map[string]bool{} + for _, e := range c.Env { + assert.NotEqual(t, common.DDAuthTokenFilePath, e.Name, "container %s should not keep DD_AUTH_TOKEN_FILE_PATH on autopilot", c.Name) + } + for _, m := range c.VolumeMounts { + mounts[m.Name] = true + assert.NotEqual(t, common.AuthVolumeName, m.Name, "container %s should not keep auth mount on autopilot", c.Name) + assert.True(t, remainingVolumes[m.Name], "mount %q should refer to an existing volume", m.Name) + } + + if c.Name == string(apicommon.SystemProbeContainerName) { + assert.False(t, mounts[common.DogstatsdSocketVolumeName], "system-probe DogStatsD socket mount should be stripped with its volume") + assert.True(t, mounts[common.ProcdirVolumeName], "system-probe proc mount should survive for NPM/service discovery") + assert.True(t, mounts[common.CgroupsVolumeName], "system-probe cgroups mount should survive for NPM/service discovery") + } + } +} + +func TestGetAutopilotAllowlistVersionAnnotation(t *testing.T) { + tests := []struct { + name string + annotation string + want string + }{ + {name: "no annotation", annotation: "", want: ""}, + {name: "explicit override", annotation: "v1.2.3", want: "v1.2.3"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dda := &v2alpha1.DatadogAgent{ + ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{}}, + } + if tt.annotation != "" { + dda.Annotations[getExperimentalAnnotationKey(ExperimentalAutopilotAllowlistVersionSubkey)] = tt.annotation + } + got := getExperimentalAnnotation(dda, ExperimentalAutopilotAllowlistVersionSubkey) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/controller/datadogagent/experimental/const.go b/internal/controller/datadogagent/experimental/const.go index 4b457003ed..7689d0f482 100644 --- a/internal/controller/datadogagent/experimental/const.go +++ b/internal/controller/datadogagent/experimental/const.go @@ -8,3 +8,4 @@ package experimental const ExperimentalAnnotationPrefix = "experimental.agent.datadoghq.com" const ExperimentalImageOverrideConfigSubkey = "image-override-config" const ExperimentalAutopilotSubkey = "autopilot" +const ExperimentalAutopilotAllowlistVersionSubkey = "autopilot-allowlist-version" diff --git a/internal/controller/datadogagent/global/dependencies.go b/internal/controller/datadogagent/global/dependencies.go index d6f3d28424..41081f9610 100644 --- a/internal/controller/datadogagent/global/dependencies.go +++ b/internal/controller/datadogagent/global/dependencies.go @@ -23,6 +23,7 @@ import ( "github.com/DataDog/datadog-operator/internal/controller/datadogagent/component/clusterchecksrunner" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/component/objects" otelagentgateway "github.com/DataDog/datadog-operator/internal/controller/datadogagent/component/otelagentgateway" + "github.com/DataDog/datadog-operator/internal/controller/datadogagent/experimental" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature" featureutils "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/utils" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/object" @@ -307,7 +308,12 @@ func nodeAgentDependencies(ddaMeta metav1.Object, ddaSpec *v2alpha1.DatadogAgent var errs []error serviceAccountName := constants.GetAgentServiceAccount(ddaMeta.GetName(), ddaSpec) rbacResourcesName := agent.GetAgentRoleName(ddaMeta) - useFineGrainedAuthorization := featureutils.HasFeatureEnableAnnotation(ddaMeta, featureutils.EnableFineGrainedKubeletAuthz) + autopilotEnabled := experimental.IsAutopilotEnabled(ddaMeta) + // Fine-grained kubelet authorization: explicit annotation OR Autopilot default. + useFineGrainedAuthorization := featureutils.HasFeatureEnableAnnotation(ddaMeta, featureutils.EnableFineGrainedKubeletAuthz) || autopilotEnabled + // Pods get/list on the API server are required when the kubelet endpoint is + // not reachable (e.g. GKE Autopilot), to support DD_KUBELET_USE_API_SERVER=true. + kubeletUseAPIServer := autopilotEnabled // Service account if err := manager.RBACManager().AddServiceAccountByComponent(ddaMeta.GetNamespace(), serviceAccountName, string(v2alpha1.NodeAgentComponentName)); err != nil { @@ -315,7 +321,7 @@ func nodeAgentDependencies(ddaMeta metav1.Object, ddaSpec *v2alpha1.DatadogAgent } // ClusterRole creation - if err := manager.RBACManager().AddClusterPolicyRulesByComponent(ddaMeta.GetNamespace(), rbacResourcesName, serviceAccountName, agent.GetDefaultAgentClusterRolePolicyRules(disableNonResourceRules(ddaSpec), useFineGrainedAuthorization), string(v2alpha1.NodeAgentComponentName)); err != nil { + if err := manager.RBACManager().AddClusterPolicyRulesByComponent(ddaMeta.GetNamespace(), rbacResourcesName, serviceAccountName, agent.GetDefaultAgentClusterRolePolicyRules(disableNonResourceRules(ddaSpec), useFineGrainedAuthorization, kubeletUseAPIServer), string(v2alpha1.NodeAgentComponentName)); err != nil { errs = append(errs, err) } diff --git a/internal/controller/datadogagent_controller.go b/internal/controller/datadogagent_controller.go index 0817dc49ca..81ce582622 100644 --- a/internal/controller/datadogagent_controller.go +++ b/internal/controller/datadogagent_controller.go @@ -61,7 +61,7 @@ type DatadogAgentReconciler struct { // +kubebuilder:rbac:groups="",resources=pods/exec,verbs=create // +kubebuilder:rbac:groups="",resources=pods/eviction,verbs=create // +kubebuilder:rbac:groups="",resources=pods/resize,verbs=patch -// +kubebuilder:rbac:groups=auto.gke.io,resources=allowlistsynchronizers,verbs=get;list;watch;create +// +kubebuilder:rbac:groups=auto.gke.io,resources=allowlistsynchronizers,verbs=get;list;watch;create;patch // Finalizer (cluster-scoped resources) // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles,verbs=deletecollection diff --git a/pkg/allowlistsynchronizer/allowlistsynchronizer.go b/pkg/allowlistsynchronizer/allowlistsynchronizer.go index 564c0eb16a..fa31547742 100644 --- a/pkg/allowlistsynchronizer/allowlistsynchronizer.go +++ b/pkg/allowlistsynchronizer/allowlistsynchronizer.go @@ -4,16 +4,27 @@ package allowlistsynchronizer import ( "context" + "fmt" + "regexp" - apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/config" logf "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/DataDog/datadog-operator/pkg/kubernetes" ) +// DefaultWorkloadAllowlistVersion is the default version of the Datadog +// daemonset WorkloadAllowlist. v1.0.3 includes the system-probe / NPM +// exemptions required by the NPM feature on GKE Autopilot. +const DefaultWorkloadAllowlistVersion = "v1.0.3" +const allowlistSynchronizerFieldOwner = "datadog-operator-allowlist-synchronizer" + +var workloadAllowlistVersionRegexp = regexp.MustCompile(`^v\d+\.\d+\.\d+$`) + var ( SchemeGroupVersion = schema.GroupVersion{ Group: "auto.gke.io", @@ -46,33 +57,62 @@ type AllowlistSynchronizerSpec struct { AllowlistPaths []string `json:"allowlistPaths,omitempty"` } -func createAllowlistSynchronizerResource(k8sClient client.Client) error { +// resolveWorkloadAllowlistVersion returns the requested allowlist version if it +// is non-empty and well-formed, otherwise it falls back to +// DefaultWorkloadAllowlistVersion (logging the malformed input). +func resolveWorkloadAllowlistVersion(version string) string { + if version == "" { + return DefaultWorkloadAllowlistVersion + } + if !workloadAllowlistVersionRegexp.MatchString(version) { + logger.Info("Ignoring malformed WorkloadAllowlist version override, falling back to default", + "requested", version, "default", DefaultWorkloadAllowlistVersion) + return DefaultWorkloadAllowlistVersion + } + return version +} + +func applyAllowlistSynchronizerResource(k8sClient client.Client, version, partOfLabel string) error { obj := &AllowlistSynchronizer{ TypeMeta: metav1.TypeMeta{ - APIVersion: "allowlistsynchronizers.auto.gke.io", + APIVersion: SchemeGroupVersion.String(), Kind: "AllowlistSynchronizer", }, ObjectMeta: metav1.ObjectMeta{ Name: "datadog-synchronizer", - Annotations: map[string]string{ - "helm.sh/hook": "pre-install,pre-upgrade", - "helm.sh/hook-weight": "-1", + Labels: map[string]string{ + "app.kubernetes.io/created-by": "datadog-operator", + kubernetes.AppKubernetesManageByLabelKey: "datadog-operator", + kubernetes.AppKubernetesNameLabelKey: "datadog-allowlist-synchronizer", + kubernetes.AppKubernetesPartOfLabelKey: partOfLabel, }, }, Spec: AllowlistSynchronizerSpec{ AllowlistPaths: []string{ - "Datadog/datadog/datadog-datadog-daemonset-exemption-v1.0.1.yaml", + fmt.Sprintf("Datadog/datadog/datadog-datadog-daemonset-exemption-%s.yaml", version), }, }, } - return k8sClient.Create(context.TODO(), obj) + return k8sClient.Patch( + context.TODO(), + obj, + client.Apply, + client.FieldOwner(allowlistSynchronizerFieldOwner), + client.ForceOwnership, + ) } // CreateAllowlistSynchronizer creates a GKE AllowlistSynchronizer Custom Resource (auto.gke.io/v1) for the Datadog WorkloadAllowlist if it doesn't exist. // The AllowlistSynchronizer is needed so that GKE Autopilot can sync the Datadog WorkloadAllowlist to the cluster. See the CRD reference: // https://cloud.google.com/kubernetes-engine/docs/reference/crds/allowlistsynchronizer -func CreateAllowlistSynchronizer() { +// +// version selects the WorkloadAllowlist YAML to point at. Pass an empty string +// to use DefaultWorkloadAllowlistVersion. Malformed versions also fall back to +// the default. +func CreateAllowlistSynchronizer(version, partOfLabel string) { + resolvedVersion := resolveWorkloadAllowlistVersion(version) + cfg, configErr := config.GetConfig() if configErr != nil { logger.Error(configErr, "failed to load kubeconfig") @@ -91,21 +131,10 @@ func CreateAllowlistSynchronizer() { return } - existing := &AllowlistSynchronizer{} - if existingErr := k8sClient.Get(context.TODO(), client.ObjectKey{Name: "datadog-synchronizer"}, existing); existingErr == nil { - return - } else if !apierrors.IsNotFound(existingErr) { - logger.Error(existingErr, "failed to check existing AllowlistSynchronizer resource") - return - } - - if err := createAllowlistSynchronizerResource(k8sClient); err != nil { - if apierrors.IsAlreadyExists(err) { - return - } - logger.Error(err, "failed to create AllowlistSynchronizer resource") + if err := applyAllowlistSynchronizerResource(k8sClient, resolvedVersion, partOfLabel); err != nil { + logger.Error(err, "failed to apply AllowlistSynchronizer resource") return } - logger.Info("Successfully created AllowlistSynchronizer") + logger.V(1).Info("Successfully applied AllowlistSynchronizer", "version", resolvedVersion) } diff --git a/pkg/allowlistsynchronizer/allowlistsynchronizer_test.go b/pkg/allowlistsynchronizer/allowlistsynchronizer_test.go new file mode 100644 index 0000000000..5d45105eb8 --- /dev/null +++ b/pkg/allowlistsynchronizer/allowlistsynchronizer_test.go @@ -0,0 +1,117 @@ +// 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 2025-present Datadog, Inc. + +package allowlistsynchronizer + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/DataDog/datadog-operator/pkg/kubernetes" +) + +func TestResolveWorkloadAllowlistVersion(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + {name: "empty falls back to default", input: "", expected: DefaultWorkloadAllowlistVersion}, + {name: "well-formed override is preserved", input: "v2.5.0", expected: "v2.5.0"}, + {name: "malformed falls back to default (no v prefix)", input: "1.0.3", expected: DefaultWorkloadAllowlistVersion}, + {name: "malformed falls back to default (extra suffix)", input: "v1.0.3-alpha", expected: DefaultWorkloadAllowlistVersion}, + {name: "malformed falls back to default (random)", input: "garbage", expected: DefaultWorkloadAllowlistVersion}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, resolveWorkloadAllowlistVersion(tt.input)) + }) + } +} + +func TestDefaultWorkloadAllowlistVersion(t *testing.T) { + // Sanity check — locks the default to a known value so a silent bump is caught. + assert.Equal(t, "v1.0.3", DefaultWorkloadAllowlistVersion) +} + +func TestApplyAllowlistSynchronizerResource_AllowlistPath(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, SchemeBuilder.AddToScheme(scheme)) + + tests := []struct { + name string + version string + expectPath string + }{ + { + name: "default version", + version: DefaultWorkloadAllowlistVersion, + expectPath: "Datadog/datadog/datadog-datadog-daemonset-exemption-v1.0.3.yaml", + }, + { + name: "user override", + version: "v2.5.0", + expectPath: "Datadog/datadog/datadog-datadog-daemonset-exemption-v2.5.0.yaml", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := fake.NewClientBuilder().WithScheme(scheme).Build() + require.NoError(t, applyAllowlistSynchronizerResource(c, tt.version, "default-foo")) + + got := &AllowlistSynchronizer{} + require.NoError(t, c.Get(context.TODO(), client.ObjectKey{Name: "datadog-synchronizer"}, got)) + require.Len(t, got.Spec.AllowlistPaths, 1) + assert.Equal(t, tt.expectPath, got.Spec.AllowlistPaths[0]) + assert.Empty(t, got.Annotations) + assert.Equal(t, "datadog-operator", got.Labels["app.kubernetes.io/created-by"]) + assert.Equal(t, "datadog-operator", got.Labels[kubernetes.AppKubernetesManageByLabelKey]) + assert.Equal(t, "datadog-allowlist-synchronizer", got.Labels[kubernetes.AppKubernetesNameLabelKey]) + assert.Equal(t, "default-foo", got.Labels[kubernetes.AppKubernetesPartOfLabelKey]) + }) + } +} + +func TestApplyAllowlistSynchronizerResource_UpdatesExistingResource(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, SchemeBuilder.AddToScheme(scheme)) + + existing := &AllowlistSynchronizer{ + TypeMeta: metav1.TypeMeta{ + APIVersion: SchemeGroupVersion.String(), + Kind: "AllowlistSynchronizer", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "datadog-synchronizer", + Labels: map[string]string{ + kubernetes.AppKubernetesPartOfLabelKey: "old-owner", + }, + }, + Spec: AllowlistSynchronizerSpec{ + AllowlistPaths: []string{ + "Datadog/datadog/datadog-datadog-daemonset-exemption-v1.0.1.yaml", + }, + }, + } + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(existing).Build() + + require.NoError(t, applyAllowlistSynchronizerResource(c, "v1.0.3", "default-foo")) + + got := &AllowlistSynchronizer{} + require.NoError(t, c.Get(context.TODO(), client.ObjectKey{Name: "datadog-synchronizer"}, got)) + require.Len(t, got.Spec.AllowlistPaths, 1) + assert.Equal(t, "Datadog/datadog/datadog-datadog-daemonset-exemption-v1.0.3.yaml", got.Spec.AllowlistPaths[0]) + assert.Equal(t, "default-foo", got.Labels[kubernetes.AppKubernetesPartOfLabelKey]) + assert.Equal(t, "datadog-operator", got.Labels[kubernetes.AppKubernetesManageByLabelKey]) + assert.Equal(t, "datadog-allowlist-synchronizer", got.Labels[kubernetes.AppKubernetesNameLabelKey]) +}