diff --git a/.github/codecov.yml b/.github/codecov.yml index dd0e7adde7..3e49ab2893 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -36,4 +36,14 @@ comment: ignore: - "test/**/*" # ignore folders and all its contents - - "**/zz_generated.*.go" # api generated files \ No newline at end of file + - "**/testutils/**/*" # test helper packages + - "**/fake/**/*" # test fake implementations + - "internal/controller/datadogagent/feature/test/**/*" # feature test harness + - "hack/**/*" # developer tooling + - "marketplaces/**/*" # marketplace packaging/release tooling + - "examples/**/*" # sample manifests + - "docs/**/*" # documentation and generated docs + - "config/**/*" # generated/deployment manifests + - "bundle/**/*" # generated OLM bundle manifests + - "api/datadoghq/**/*_types.go" # CRD schema type declarations + - "**/zz_generated.*.go" # api generated files diff --git a/cmd/yaml-mapper/utils/maps_test.go b/cmd/yaml-mapper/utils/maps_test.go new file mode 100644 index 0000000000..2c93b5fb1c --- /dev/null +++ b/cmd/yaml-mapper/utils/maps_test.go @@ -0,0 +1,168 @@ +// 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 utils + +import ( + "testing" + + "github.com/stretchr/testify/require" + "helm.sh/helm/v3/pkg/chartutil" +) + +func TestInsertAtPath(t *testing.T) { + values := map[string]any{ + "datadog": map[string]any{ + "apiKey": "existing", + }, + } + + InsertAtPath("datadog.logs.enabled", true, values) + + require.Equal(t, "existing", values["datadog"].(map[string]any)["apiKey"]) + require.Equal(t, true, values["datadog"].(map[string]any)["logs"].(map[string]any)["enabled"]) +} + +func TestMergeMapDeep(t *testing.T) { + t.Run("deep merges nested maps", func(t *testing.T) { + left := map[string]any{ + "datadog": map[string]any{ + "apiKey": "existing", + }, + } + right := map[string]any{ + "datadog": map[string]any{ + "logs": map[string]any{"enabled": true}, + }, + } + + got := MergeMapDeep(left, right) + + require.Equal(t, "existing", got["datadog"].(map[string]any)["apiKey"]) + require.Equal(t, true, got["datadog"].(map[string]any)["logs"].(map[string]any)["enabled"]) + }) + + t.Run("overwrites scalars and ignores nil values", func(t *testing.T) { + got := MergeMapDeep( + map[string]any{"logs": false, "apm": true}, + map[string]any{"logs": true, "apm": nil}, + ) + + require.Equal(t, true, got["logs"]) + require.Equal(t, true, got["apm"]) + }) +} + +func TestMergeOrSet(t *testing.T) { + t.Run("merges map values", func(t *testing.T) { + values := map[string]any{ + "datadog": map[string]any{"apiKey": "existing"}, + } + + MergeOrSet(values, "datadog", chartutil.Values{"logs": map[string]any{"enabled": true}}) + + require.Equal(t, "existing", values["datadog"].(map[string]any)["apiKey"]) + require.Equal(t, true, values["datadog"].(map[string]any)["logs"].(map[string]any)["enabled"]) + }) + + t.Run("does not write nil values", func(t *testing.T) { + values := map[string]any{} + + MergeOrSet(values, "datadog", nil) + + require.Empty(t, values) + }) +} + +func TestGetPathHelpers(t *testing.T) { + values := chartutil.Values{ + "datadog": map[string]any{ + "apiKey": "abc", + "logs": map[string]any{ + "enabled": true, + "items": []any{"first"}, + }, + }, + } + + stringValue, ok := GetPathString(values, "datadog", "apiKey") + require.True(t, ok) + require.Equal(t, "abc", stringValue) + + boolValue, ok := GetPathBool(values, "datadog", "logs", "enabled") + require.True(t, ok) + require.True(t, boolValue) + + sliceValue, ok := GetPathSlice(values, "datadog", "logs", "items") + require.True(t, ok) + require.Equal(t, []any{"first"}, sliceValue) + + mapValue, ok := GetPathMap(values, "datadog", "logs") + require.True(t, ok) + require.Equal(t, true, mapValue["enabled"]) + + _, ok = GetPathString(values, "datadog", "logs", "enabled") + require.False(t, ok) + + _, ok = GetPathVal(values, "datadog", "missing") + require.False(t, ok) +} + +func TestApplyDeprecationRules(t *testing.T) { + t.Run("moves deprecated boolean aliases to their standard key", func(t *testing.T) { + values := chartutil.Values{ + "datadog": map[string]any{ + "apm": map[string]any{ + "enabled": false, + }, + }, + } + + got := ApplyDeprecationRules(values) + + portEnabled, ok := GetPathBool(got, "datadog", "apm", "portEnabled") + require.True(t, ok) + require.False(t, portEnabled) + + _, ok = GetPathBool(got, "datadog", "apm", "enabled") + require.False(t, ok) + }) + + t.Run("standard key participates in boolean OR mapping", func(t *testing.T) { + values := chartutil.Values{ + "datadog": map[string]any{ + "apm": map[string]any{ + "enabled": false, + "portEnabled": true, + }, + }, + } + + got := ApplyDeprecationRules(values) + + portEnabled, ok := GetPathBool(got, "datadog", "apm", "portEnabled") + require.True(t, ok) + require.True(t, portEnabled) + }) + + t.Run("negates deprecated inverse keys unless the standard key is present", func(t *testing.T) { + values := chartutil.Values{ + "datadog": map[string]any{ + "systemProbe": map[string]any{ + "enableDefaultOsReleasePaths": false, + }, + }, + } + + got := ApplyDeprecationRules(values) + + disableDefaultPaths, ok := GetPathBool(got, "datadog", "disableDefaultOsReleasePaths") + require.True(t, ok) + require.True(t, disableDefaultPaths) + + _, ok = GetPathBool(got, "datadog", "systemProbe", "enableDefaultOsReleasePaths") + require.False(t, ok) + }) +} diff --git a/code-coverage.datadog.yml b/code-coverage.datadog.yml index 9bf1151cee..3282c34587 100644 --- a/code-coverage.datadog.yml +++ b/code-coverage.datadog.yml @@ -1,6 +1,16 @@ schema-version: v1 ignore: - "test/" + - "**/testutils/" + - "**/fake/" + - "internal/controller/datadogagent/feature/test/" + - "hack/" + - "marketplaces/" + - "examples/" + - "docs/" + - "config/" + - "bundle/" + - "api/datadoghq/**/*_types.go" - "**/zz_generated.*.go" gates: - type: patch_coverage_percentage diff --git a/internal/controller/datadogagent/common/utils_test.go b/internal/controller/datadogagent/common/utils_test.go index b9b70e3b6a..8b9428e775 100644 --- a/internal/controller/datadogagent/common/utils_test.go +++ b/internal/controller/datadogagent/common/utils_test.go @@ -4,3 +4,154 @@ // Copyright 2025-present Datadog, Inc. package common + +import ( + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/version" + + apicommon "github.com/DataDog/datadog-operator/api/datadoghq/common" + "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" + "github.com/DataDog/datadog-operator/pkg/constants" + "github.com/DataDog/datadog-operator/pkg/kubernetes" +) + +func TestNewDeploymentUsesDefaultMetadata(t *testing.T) { + dda := testDatadogAgent() + + deployment := NewDeployment(dda, constants.DefaultClusterAgentResourceSuffix, "datadog-cluster-agent", "7.77.0", nil) + + require.Equal(t, "datadog-cluster-agent", deployment.Name) + require.Equal(t, "agents", deployment.Namespace) + require.Equal(t, "datadog-cluster-agent", deployment.Labels[kubernetes.AppKubernetesInstanceLabelKey]) + require.Equal(t, constants.DefaultClusterAgentResourceSuffix, deployment.Labels[apicommon.AgentDeploymentComponentLabelKey]) + require.Equal(t, map[string]string{ + kubernetes.AppKubernetesInstanceLabelKey: "datadog-cluster-agent", + apicommon.AgentDeploymentComponentLabelKey: constants.DefaultClusterAgentResourceSuffix, + }, deployment.Spec.Selector.MatchLabels) +} + +func TestGetDefaultMetadata(t *testing.T) { + t.Run("copies explicit selector labels into object labels", func(t *testing.T) { + selector := &metav1.LabelSelector{MatchLabels: map[string]string{"custom": "selector"}} + + labels, _, gotSelector := GetDefaultMetadata(testDatadogAgent(), constants.DefaultAgentResourceSuffix, "datadog-agent", "7.77.0", selector) + + require.Equal(t, selector, gotSelector) + require.Equal(t, "selector", labels["custom"]) + }) + + t.Run("uses legacy selector labels when metadata update is disabled", func(t *testing.T) { + dda := testDatadogAgent() + dda.Annotations = map[string]string{apicommon.UpdateMetadataAnnotationKey: "false"} + + _, _, selector := GetDefaultMetadata(dda, constants.DefaultAgentResourceSuffix, "datadog-agent", "7.77.0", nil) + + require.Equal(t, map[string]string{ + apicommon.AgentDeploymentNameLabelKey: "datadog", + apicommon.AgentDeploymentComponentLabelKey: constants.DefaultAgentResourceSuffix, + }, selector.MatchLabels) + }) +} + +func TestComponentVersionHelpers(t *testing.T) { + t.Run("uses the component image tag override", func(t *testing.T) { + dda := testDatadogAgent() + dda.Spec.Override = map[v2alpha1.ComponentName]*v2alpha1.DatadogAgentComponentOverride{ + v2alpha1.NodeAgentComponentName: { + Image: &v2alpha1.AgentImageConfig{Tag: "7.76.0"}, + }, + } + + require.Equal(t, "7.76.0", GetComponentVersion(dda, v2alpha1.NodeAgentComponentName)) + }) + + t.Run("extracts a version from image name and removes jmx suffix", func(t *testing.T) { + require.Equal(t, "7.75.1", GetAgentVersionFromImage(v2alpha1.AgentImageConfig{ + Name: "gcr.io/datadoghq/agent:7.75.1-jmx", + })) + }) + + t.Run("image tag takes precedence over image name", func(t *testing.T) { + require.Equal(t, "7.77.0", GetAgentVersionFromImage(v2alpha1.AgentImageConfig{ + Name: "gcr.io/datadoghq/agent:7.75.1", + Tag: "7.77.0", + })) + }) +} + +func TestEnvVarBuilders(t *testing.T) { + source := BuildEnvVarFromSecret("datadog-secret", "api-key") + envVar := BuildEnvVarFromSource("DD_API_KEY", source) + + require.Equal(t, "DD_API_KEY", envVar.Name) + require.Equal(t, "datadog-secret", envVar.ValueFrom.SecretKeyRef.Name) + require.Equal(t, "api-key", envVar.ValueFrom.SecretKeyRef.Key) +} + +func TestServiceSelectors(t *testing.T) { + dda := testDatadogAgent() + + require.Equal(t, map[string]string{ + kubernetes.AppKubernetesPartOfLabelKey: "agents-datadog", + apicommon.AgentDeploymentComponentLabelKey: constants.DefaultAgentResourceSuffix, + }, GetAgentLocalServiceSelector(dda)) + + require.Equal(t, map[string]string{ + kubernetes.AppKubernetesPartOfLabelKey: "agents-datadog", + apicommon.AgentDeploymentComponentLabelKey: constants.DefaultOtelAgentGatewayResourceSuffix, + }, GetOtelAgentGatewayServiceSelector(dda)) +} + +func TestShouldCreateAgentLocalService(t *testing.T) { + require.False(t, ShouldCreateAgentLocalService(nil, true)) + require.False(t, ShouldCreateAgentLocalService(&version.Info{}, true)) + require.False(t, ShouldCreateAgentLocalService(&version.Info{GitVersion: "v1.21.0"}, false)) + require.True(t, ShouldCreateAgentLocalService(&version.Info{GitVersion: "v1.21.0"}, true)) + require.True(t, ShouldCreateAgentLocalService(&version.Info{GitVersion: "v1.22.0"}, false)) +} + +func TestMergeAffinities(t *testing.T) { + first := &corev1.Affinity{ + NodeAffinity: &corev1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{ + {MatchExpressions: []corev1.NodeSelectorRequirement{{Key: "disk", Operator: corev1.NodeSelectorOpIn, Values: []string{"ssd"}}}}, + }, + }, + }, + PodAffinity: &corev1.PodAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: []corev1.PodAffinityTerm{{TopologyKey: "zone-a"}}, + }, + } + second := &corev1.Affinity{ + NodeAffinity: &corev1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ + NodeSelectorTerms: []corev1.NodeSelectorTerm{ + {MatchExpressions: []corev1.NodeSelectorRequirement{{Key: "arch", Operator: corev1.NodeSelectorOpIn, Values: []string{"arm64"}}}}, + }, + }, + }, + PodAffinity: &corev1.PodAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: []corev1.PodAffinityTerm{{TopologyKey: "zone-b"}}, + }, + } + + merged := MergeAffinities(first, second) + + require.Len(t, merged.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms, 1) + require.Len(t, merged.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions, 2) + require.Equal(t, []corev1.PodAffinityTerm{{TopologyKey: "zone-a"}, {TopologyKey: "zone-b"}}, merged.PodAffinity.RequiredDuringSchedulingIgnoredDuringExecution) +} + +func testDatadogAgent() *v2alpha1.DatadogAgent { + return &v2alpha1.DatadogAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "datadog", + Namespace: "agents", + }, + } +} diff --git a/internal/controller/datadogagent/component/agent/new_test.go b/internal/controller/datadogagent/component/agent/new_test.go new file mode 100644 index 0000000000..f66b38726c --- /dev/null +++ b/internal/controller/datadogagent/component/agent/new_test.go @@ -0,0 +1,80 @@ +// 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 agent + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + apicommon "github.com/DataDog/datadog-operator/api/datadoghq/common" + "github.com/DataDog/datadog-operator/pkg/constants" + "github.com/DataDog/datadog-operator/pkg/kubernetes" +) + +func TestNewDaemonset(t *testing.T) { + dda := &metav1.ObjectMeta{Name: "datadog", Namespace: "agents"} + + daemonset := NewDaemonset( + dda, + &ExtendedDaemonsetOptions{MaxPodUnavailable: "25%"}, + constants.DefaultAgentResourceSuffix, + "datadog-agent", + "7.78.0", + nil, + "datadog-agent", + ) + + require.Equal(t, "datadog-agent", daemonset.Name) + require.Equal(t, "agents", daemonset.Namespace) + require.Equal(t, "datadog-agent", daemonset.Labels[kubernetes.AppKubernetesInstanceLabelKey]) + require.Equal(t, constants.DefaultAgentResourceSuffix, daemonset.Labels[apicommon.AgentDeploymentComponentLabelKey]) + require.Equal(t, intstr.FromString("25%"), *daemonset.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable) + require.Equal(t, map[string]string{ + kubernetes.AppKubernetesInstanceLabelKey: "datadog-agent", + apicommon.AgentDeploymentComponentLabelKey: constants.DefaultAgentResourceSuffix, + }, daemonset.Spec.Selector.MatchLabels) +} + +func TestNewExtendedDaemonset(t *testing.T) { + dda := &metav1.ObjectMeta{Name: "datadog", Namespace: "agents"} + + extendedDaemonSet := NewExtendedDaemonset( + dda, + &ExtendedDaemonsetOptions{ + MaxPodUnavailable: "25%", + MaxPodSchedulerFailure: "10%", + SlowStartAdditiveIncrease: "2", + CanaryDuration: 5 * time.Minute, + CanaryReplicas: "2", + CanaryAutoPauseEnabled: true, + CanaryAutoPauseMaxRestarts: 3, + CanaryAutoFailEnabled: true, + CanaryAutoFailMaxRestarts: 4, + CanaryAutoPauseMaxSlowStartDuration: time.Minute, + }, + constants.DefaultAgentResourceSuffix, + "datadog-agent", + "7.78.0", + nil, + ) + + require.Equal(t, "datadog-agent", extendedDaemonSet.Name) + require.Equal(t, "agents", extendedDaemonSet.Namespace) + require.Equal(t, "25%", extendedDaemonSet.Spec.Strategy.RollingUpdate.MaxUnavailable.StrVal) + require.Equal(t, "10%", extendedDaemonSet.Spec.Strategy.RollingUpdate.MaxPodSchedulerFailure.StrVal) + require.Equal(t, intstr.FromInt(2), *extendedDaemonSet.Spec.Strategy.RollingUpdate.SlowStartAdditiveIncrease) + require.Equal(t, 5*time.Minute, extendedDaemonSet.Spec.Strategy.Canary.Duration.Duration) + require.Equal(t, intstr.FromInt(2), *extendedDaemonSet.Spec.Strategy.Canary.Replicas) + require.True(t, *extendedDaemonSet.Spec.Strategy.Canary.AutoPause.Enabled) + require.Equal(t, int32(3), *extendedDaemonSet.Spec.Strategy.Canary.AutoPause.MaxRestarts) + require.True(t, *extendedDaemonSet.Spec.Strategy.Canary.AutoFail.Enabled) + require.Equal(t, int32(4), *extendedDaemonSet.Spec.Strategy.Canary.AutoFail.MaxRestarts) + require.Equal(t, time.Minute, extendedDaemonSet.Spec.Strategy.Canary.AutoPause.MaxSlowStartDuration.Duration) +} 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..bca02ed959 --- /dev/null +++ b/internal/controller/datadogagent/component/agent/rbac_test.go @@ -0,0 +1,51 @@ +// 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 agent + +import ( + "testing" + + "github.com/stretchr/testify/require" + rbacv1 "k8s.io/api/rbac/v1" + + "github.com/DataDog/datadog-operator/pkg/kubernetes/rbac" +) + +func TestGetDefaultAgentClusterRolePolicyRules(t *testing.T) { + withNonResourceRules := GetDefaultAgentClusterRolePolicyRules(false, false) + withoutNonResourceRules := GetDefaultAgentClusterRolePolicyRules(true, false) + + require.Len(t, withNonResourceRules, len(withoutNonResourceRules)+1) + require.Contains(t, withNonResourceRules, getMetricsEndpointPolicyRule()) + require.NotContains(t, withoutNonResourceRules, getMetricsEndpointPolicyRule()) +} + +func TestGetKubeletPolicyRule(t *testing.T) { + require.Equal(t, rbacv1.PolicyRule{ + APIGroups: []string{rbac.CoreAPIGroup}, + Resources: []string{ + rbac.NodeMetricsResource, + rbac.NodeSpecResource, + rbac.NodeStats, + rbac.NodePodsResource, + rbac.NodeHealthzResource, + rbac.NodeConfigzResource, + rbac.NodeLogsResource, + }, + Verbs: []string{rbac.GetVerb}, + }, getKubeletPolicyRule(true)) + + require.Equal(t, rbacv1.PolicyRule{ + APIGroups: []string{rbac.CoreAPIGroup}, + Resources: []string{ + rbac.NodeMetricsResource, + rbac.NodeSpecResource, + rbac.NodeProxyResource, + rbac.NodeStats, + }, + Verbs: []string{rbac.GetVerb}, + }, getKubeletPolicyRule(false)) +} diff --git a/internal/controller/datadogagent/component/clusterchecksrunner/default_test.go b/internal/controller/datadogagent/component/clusterchecksrunner/default_test.go index 0b43ec1ccd..ad15180e68 100644 --- a/internal/controller/datadogagent/component/clusterchecksrunner/default_test.go +++ b/internal/controller/datadogagent/component/clusterchecksrunner/default_test.go @@ -8,9 +8,13 @@ package clusterchecksrunner import ( "testing" + apicommon "github.com/DataDog/datadog-operator/api/datadoghq/common" "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" + "github.com/DataDog/datadog-operator/pkg/constants" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" policyv1 "k8s.io/api/policy/v1" + policyv1beta1 "k8s.io/api/policy/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" ) @@ -38,3 +42,43 @@ func Test_getPodDisruptionBudget(t *testing.T) { assert.Equal(t, intstr.FromInt(pdbMaxUnavailableInstances), *testpdb.Spec.MaxUnavailable) assert.Nil(t, testpdb.Spec.MinAvailable) } + +func Test_getPodDisruptionBudget_v1beta1(t *testing.T) { + dda := v2alpha1.DatadogAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-datadog-agent", + Namespace: "some-namespace", + }, + } + + testpdb := GetClusterChecksRunnerPodDisruptionBudget(&dda, true).(*policyv1beta1.PodDisruptionBudget) + + require.Equal(t, "my-datadog-agent-cluster-checks-runner-pdb", testpdb.Name) + require.Equal(t, "some-namespace", testpdb.Namespace) + require.Equal(t, intstr.FromInt(pdbMaxUnavailableInstances), *testpdb.Spec.MaxUnavailable) + require.Equal(t, map[string]string{ + apicommon.AgentDeploymentNameLabelKey: "my-datadog-agent", + apicommon.AgentDeploymentComponentLabelKey: constants.DefaultClusterChecksRunnerResourceSuffix, + }, testpdb.Spec.Selector.MatchLabels) +} + +func TestClusterChecksRunnerDefaultDeployment(t *testing.T) { + dda := v2alpha1.DatadogAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-datadog-agent", + Namespace: "some-namespace", + }, + } + + deployment := NewDefaultClusterChecksRunnerDeployment(&dda, &dda.Spec) + + require.Equal(t, "my-datadog-agent-cluster-checks-runner", deployment.Name) + require.Equal(t, "some-namespace", deployment.Namespace) + require.NotNil(t, deployment.Spec.Replicas) + require.Equal(t, int32(defaultClusterChecksRunnerReplicas), *deployment.Spec.Replicas) + require.Equal(t, deployment.Labels, deployment.Spec.Template.Labels) + require.Equal(t, deployment.Annotations, deployment.Spec.Template.Annotations) + require.Equal(t, getDefaultServiceAccountName(&dda), deployment.Spec.Template.Spec.ServiceAccountName) + require.Len(t, deployment.Spec.Template.Spec.Containers, 1) + require.Equal(t, string(apicommon.ClusterChecksRunnersContainerName), deployment.Spec.Template.Spec.Containers[0].Name) +} diff --git a/internal/controller/datadogagent/component/clusterchecksrunner/rbac_test.go b/internal/controller/datadogagent/component/clusterchecksrunner/rbac_test.go new file mode 100644 index 0000000000..2a3d996467 --- /dev/null +++ b/internal/controller/datadogagent/component/clusterchecksrunner/rbac_test.go @@ -0,0 +1,33 @@ +// 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 clusterchecksrunner + +import ( + "testing" + + "github.com/stretchr/testify/require" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/DataDog/datadog-operator/pkg/kubernetes/rbac" +) + +func TestGetDefaultClusterChecksRunnerClusterRolePolicyRules(t *testing.T) { + dda := &metav1.ObjectMeta{Name: "datadog"} + + withNonResourceRules := GetDefaultClusterChecksRunnerClusterRolePolicyRules(dda, false) + withoutNonResourceRules := GetDefaultClusterChecksRunnerClusterRolePolicyRules(dda, true) + + require.Len(t, withNonResourceRules, len(withoutNonResourceRules)+1) + require.Contains(t, withNonResourceRules, rbacv1.PolicyRule{ + NonResourceURLs: []string{rbac.MetricsURL, rbac.MetricsSLIsURL}, + Verbs: []string{rbac.GetVerb}, + }) + require.NotContains(t, withoutNonResourceRules, rbacv1.PolicyRule{ + NonResourceURLs: []string{rbac.MetricsURL, rbac.MetricsSLIsURL}, + Verbs: []string{rbac.GetVerb}, + }) +} diff --git a/internal/controller/datadogagent/component/objects/network_test.go b/internal/controller/datadogagent/component/objects/network_test.go new file mode 100644 index 0000000000..87fd6316bc --- /dev/null +++ b/internal/controller/datadogagent/component/objects/network_test.go @@ -0,0 +1,369 @@ +// 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 objects + +import ( + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + 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/pkg/cilium/v1" + "github.com/DataDog/datadog-operator/pkg/constants" + "github.com/DataDog/datadog-operator/pkg/equality" + "github.com/DataDog/datadog-operator/pkg/kubernetes" +) + +func TestGetNetworkPolicyMetadata(t *testing.T) { + dda := testDatadogAgentObject() + + tests := []struct { + name string + component v2alpha1.ComponentName + wantName string + wantSuffix string + }{ + { + name: "node agent", + component: v2alpha1.NodeAgentComponentName, + wantName: "datadog-agent", + wantSuffix: constants.DefaultAgentResourceSuffix, + }, + { + name: "cluster agent", + component: v2alpha1.ClusterAgentComponentName, + wantName: "datadog-cluster-agent", + wantSuffix: constants.DefaultClusterAgentResourceSuffix, + }, + { + name: "cluster checks runner", + component: v2alpha1.ClusterChecksRunnerComponentName, + wantName: "datadog-cluster-checks-runner", + wantSuffix: constants.DefaultClusterChecksRunnerResourceSuffix, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + policyName, selector := GetNetworkPolicyMetadata(dda, tt.component) + + require.Equal(t, tt.wantName, policyName) + require.Equal(t, tt.wantSuffix, selector.MatchLabels[apicommon.AgentDeploymentComponentLabelKey]) + require.Equal(t, "agents-datadog", selector.MatchLabels[kubernetes.AppKubernetesPartOfLabelKey]) + }) + } +} + +func TestBuildKubernetesNetworkPolicy(t *testing.T) { + dda := testDatadogAgentObject() + + tests := []struct { + name string + component v2alpha1.ComponentName + }{ + {name: "node agent", component: v2alpha1.NodeAgentComponentName}, + {name: "cluster agent", component: v2alpha1.ClusterAgentComponentName}, + {name: "cluster checks runner", component: v2alpha1.ClusterChecksRunnerComponentName}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + policyName, namespace, selector, policyTypes, ingress, egress := BuildKubernetesNetworkPolicy(dda, tt.component) + got := &netv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: policyName, + Namespace: namespace, + }, + Spec: netv1.NetworkPolicySpec{ + PodSelector: selector, + PolicyTypes: policyTypes, + Ingress: ingress, + Egress: egress, + }, + } + want := expectedKubernetesNetworkPolicy(dda, tt.component) + + require.True(t, equality.IsEqualObject(kubernetes.NetworkPoliciesKind, got, want)) + }) + } +} + +func TestBuildCiliumPolicy(t *testing.T) { + dda := testDatadogAgentObject() + dnsSelector := metav1.LabelSelector{ + MatchLabels: map[string]string{"k8s-app": "kube-dns"}, + } + + t.Run("node agent", func(t *testing.T) { + policyName, namespace, specs := BuildCiliumPolicy( + dda, + "datadoghq.com", + "https://custom-intake.example.com", + false, + []metav1.LabelSelector{dnsSelector}, + v2alpha1.NodeAgentComponentName, + ) + + require.Equal(t, "datadog-agent", policyName) + require.Equal(t, "agents", namespace) + require.ElementsMatch(t, []string{ + "Egress to ECS agent port 51678", + "Egress to ntp", + "Egress to metadata server", + "Egress to DNS", + "Egress to Datadog intake", + "Egress to kubelet", + "Ingress for dogstatsd", + "Egress to anything for checks", + }, ciliumSpecDescriptions(specs)) + requireCiliumEndpointSelector(t, specs, constants.DefaultAgentResourceSuffix) + + dns := findCiliumSpec(t, specs, "Egress to DNS") + require.Equal(t, []metav1.LabelSelector{dnsSelector}, dns.Egress[0].ToEndpoints) + require.Equal(t, cilium.ProtocolAny, dns.Egress[0].ToPorts[0].Ports[0].Protocol) + require.Equal(t, []cilium.FQDNSelector{{MatchPattern: "*"}}, dns.Egress[0].ToPorts[0].Rules.DNS) + + intake := findCiliumSpec(t, specs, "Egress to Datadog intake") + require.Contains(t, intake.Egress[0].ToFQDNs, cilium.FQDNSelector{MatchName: "custom-intake.example.com"}) + require.Contains(t, intake.Egress[0].ToFQDNs, cilium.FQDNSelector{MatchName: "api.datadoghq.com"}) + require.Contains(t, intake.Egress[0].ToFQDNs, cilium.FQDNSelector{MatchName: "agent-http-intake.logs.datadoghq.com"}) + require.Contains(t, intake.Egress[0].ToPorts[0].Ports, cilium.PortProtocol{Port: "10516", Protocol: cilium.ProtocolTCP}) + + dogstatsd := findCiliumSpec(t, specs, "Ingress for dogstatsd") + require.Equal(t, "8125", dogstatsd.Ingress[0].ToPorts[0].Ports[0].Port) + require.Equal(t, cilium.ProtocolUDP, dogstatsd.Ingress[0].ToPorts[0].Ports[0].Protocol) + }) + + t.Run("cluster agent", func(t *testing.T) { + policyName, namespace, specs := BuildCiliumPolicy( + dda, + "datadoghq.com", + "https://custom-intake.example.com", + false, + []metav1.LabelSelector{dnsSelector}, + v2alpha1.ClusterAgentComponentName, + ) + + require.Equal(t, "datadog-cluster-agent", policyName) + require.Equal(t, "agents", namespace) + require.ElementsMatch(t, []string{ + "Egress to metadata server", + "Egress to DNS", + "Egress to Datadog intake", + "Egress to Kube API Server", + "Ingress from agent", + "Ingress from cluster agent", + "Egress to cluster agent", + }, ciliumSpecDescriptions(specs)) + requireCiliumEndpointSelector(t, specs, constants.DefaultClusterAgentResourceSuffix) + + kubeAPI := findCiliumSpec(t, specs, "Egress to Kube API Server") + require.Equal(t, []cilium.Entity{cilium.EntityKubeApiServer}, kubeAPI.Egress[0].ToEntities) + + ingressFromAgent := findCiliumSpec(t, specs, "Ingress from agent") + require.Empty(t, ingressFromAgent.Ingress[0].FromEntities) + require.Equal(t, "datadog-agent", ingressFromAgent.Ingress[0].FromEndpoints[0].MatchLabels[kubernetes.AppKubernetesInstanceLabelKey]) + require.Equal(t, "agents-datadog", ingressFromAgent.Ingress[0].FromEndpoints[0].MatchLabels[kubernetes.AppKubernetesPartOfLabelKey]) + + intake := findCiliumSpec(t, specs, "Egress to Datadog intake") + require.Contains(t, intake.Egress[0].ToFQDNs, cilium.FQDNSelector{MatchName: "custom-intake.example.com"}) + require.Contains(t, intake.Egress[0].ToFQDNs, cilium.FQDNSelector{MatchName: "orchestrator.datadoghq.com"}) + }) + + t.Run("cluster checks runner", func(t *testing.T) { + policyName, namespace, specs := BuildCiliumPolicy( + dda, + "datadoghq.com", + "https://custom-intake.example.com", + false, + []metav1.LabelSelector{dnsSelector}, + v2alpha1.ClusterChecksRunnerComponentName, + ) + + require.Equal(t, "datadog-cluster-checks-runner", policyName) + require.Equal(t, "agents", namespace) + require.ElementsMatch(t, []string{ + "Egress to metadata server", + "Egress to DNS", + "Egress to Datadog intake", + "Egress to cluster agent", + "Egress to anything for checks", + }, ciliumSpecDescriptions(specs)) + requireCiliumEndpointSelector(t, specs, constants.DefaultClusterChecksRunnerResourceSuffix) + + egressToDCA := findCiliumSpec(t, specs, "Egress to cluster agent") + require.Equal(t, "datadog-cluster-agent", egressToDCA.Egress[0].ToEndpoints[0].MatchLabels[kubernetes.AppKubernetesInstanceLabelKey]) + require.Equal(t, "agents-datadog", egressToDCA.Egress[0].ToEndpoints[0].MatchLabels[kubernetes.AppKubernetesPartOfLabelKey]) + + checks := findCiliumSpec(t, specs, "Egress to anything for checks") + require.Equal(t, "k8s:io.kubernetes.pod.namespace", checks.Egress[0].ToEndpoints[0].MatchExpressions[0].Key) + require.Equal(t, metav1.LabelSelectorOpExists, checks.Egress[0].ToEndpoints[0].MatchExpressions[0].Operator) + }) +} + +func TestBuildCiliumPolicyUsesHostEntitiesForHostNetworkClusterAgent(t *testing.T) { + _, _, specs := BuildCiliumPolicy( + testDatadogAgentObject(), + "datadoghq.com", + "", + true, + nil, + v2alpha1.ClusterAgentComponentName, + ) + + ingressFromAgent := findCiliumSpec(t, specs, "Ingress from agent") + require.Equal(t, []cilium.IngressRule{ + { + FromEntities: []cilium.Entity{cilium.EntityHost, cilium.EntityRemoteNode}, + ToPorts: []cilium.PortRule{ + { + Ports: []cilium.PortProtocol{ + {Port: "5000", Protocol: cilium.ProtocolTCP}, + {Port: "5005", Protocol: cilium.ProtocolTCP}, + }, + }, + }, + }, + }, ingressFromAgent.Ingress) +} + +func TestDefaultDDFQDNs(t *testing.T) { + got := defaultDDFQDNs("datadoghq.com", "https://custom-intake.example.com") + + require.Equal(t, []cilium.FQDNSelector{ + {MatchName: "custom-intake.example.com"}, + {MatchPattern: "*-app.agent.datadoghq.com"}, + }, got) +} + +func findCiliumSpec(t *testing.T, specs []cilium.NetworkPolicySpec, description string) cilium.NetworkPolicySpec { + t.Helper() + for _, spec := range specs { + if spec.Description == description { + return spec + } + } + t.Fatalf("missing cilium spec with description %q", description) + return cilium.NetworkPolicySpec{} +} + +func ciliumSpecDescriptions(specs []cilium.NetworkPolicySpec) []string { + descriptions := make([]string, 0, len(specs)) + for _, spec := range specs { + descriptions = append(descriptions, spec.Description) + } + return descriptions +} + +func requireCiliumEndpointSelector(t *testing.T, specs []cilium.NetworkPolicySpec, componentSuffix string) { + t.Helper() + for _, spec := range specs { + require.Equal(t, componentSuffix, spec.EndpointSelector.MatchLabels[apicommon.AgentDeploymentComponentLabelKey]) + require.Equal(t, "agents-datadog", spec.EndpointSelector.MatchLabels[kubernetes.AppKubernetesPartOfLabelKey]) + } +} + +func expectedKubernetesNetworkPolicy(dda metav1.Object, componentName v2alpha1.ComponentName) *netv1.NetworkPolicy { + policyName, podSelector := GetNetworkPolicyMetadata(dda, componentName) + dcaPort := intstr.FromInt(common.DefaultClusterAgentServicePort) + apiServerPort := intstr.FromInt(6443) + prometheusPort := intstr.FromInt(5000) + + policy := &netv1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: policyName, + Namespace: dda.GetNamespace(), + }, + Spec: netv1.NetworkPolicySpec{ + PodSelector: podSelector, + PolicyTypes: []netv1.PolicyType{netv1.PolicyTypeIngress, netv1.PolicyTypeEgress}, + }, + } + + switch componentName { + case v2alpha1.NodeAgentComponentName: + policy.Spec.Egress = []netv1.NetworkPolicyEgressRule{ + {Ports: []netv1.NetworkPolicyPort{ + {Port: intstrPtr(intstr.FromInt(443))}, + {Protocol: protocolPtr(corev1.ProtocolUDP), Port: intstrPtr(intstr.FromInt(123))}, + {Port: intstrPtr(intstr.FromInt(80))}, + {Protocol: protocolPtr(corev1.ProtocolUDP), Port: intstrPtr(intstr.FromInt(53))}, + {Protocol: protocolPtr(corev1.ProtocolTCP), Port: intstrPtr(intstr.FromInt(53))}, + {Port: intstrPtr(intstr.FromInt(10250))}, + {}, + }}, + } + case v2alpha1.ClusterAgentComponentName: + _, nodeAgentSelector := GetNetworkPolicyMetadata(dda, v2alpha1.NodeAgentComponentName) + _, ccrSelector := GetNetworkPolicyMetadata(dda, v2alpha1.ClusterChecksRunnerComponentName) + policy.Spec.Egress = []netv1.NetworkPolicyEgressRule{ + {Ports: []netv1.NetworkPolicyPort{{Port: intstrPtr(intstr.FromInt(443))}}}, + {Ports: []netv1.NetworkPolicyPort{{Port: &apiServerPort}}}, + {Ports: []netv1.NetworkPolicyPort{{Port: intstrPtr(intstr.FromInt(80))}}}, + { + Ports: []netv1.NetworkPolicyPort{{Port: &dcaPort}}, + To: []netv1.NetworkPolicyPeer{{PodSelector: &podSelector}}, + }, + {Ports: []netv1.NetworkPolicyPort{ + {Protocol: protocolPtr(corev1.ProtocolUDP), Port: intstrPtr(intstr.FromInt(53))}, + {Protocol: protocolPtr(corev1.ProtocolTCP), Port: intstrPtr(intstr.FromInt(53))}, + }}, + } + policy.Spec.Ingress = []netv1.NetworkPolicyIngressRule{ + { + Ports: []netv1.NetworkPolicyPort{{Port: &dcaPort}}, + From: []netv1.NetworkPolicyPeer{ + {PodSelector: &nodeAgentSelector}, + {PodSelector: &podSelector}, + {PodSelector: &ccrSelector}, + }, + }, + { + Ports: []netv1.NetworkPolicyPort{{Port: &prometheusPort}}, + From: []netv1.NetworkPolicyPeer{{PodSelector: &nodeAgentSelector}}, + }, + } + case v2alpha1.ClusterChecksRunnerComponentName: + _, dcaSelector := GetNetworkPolicyMetadata(dda, v2alpha1.ClusterAgentComponentName) + policy.Spec.Egress = []netv1.NetworkPolicyEgressRule{ + { + Ports: []netv1.NetworkPolicyPort{{Port: &dcaPort}}, + To: []netv1.NetworkPolicyPeer{{PodSelector: &dcaSelector}}, + }, + {Ports: []netv1.NetworkPolicyPort{ + {Port: intstrPtr(intstr.FromInt(443))}, + {Protocol: protocolPtr(corev1.ProtocolUDP), Port: intstrPtr(intstr.FromInt(123))}, + {Protocol: protocolPtr(corev1.ProtocolUDP), Port: intstrPtr(intstr.FromInt(53))}, + {Protocol: protocolPtr(corev1.ProtocolTCP), Port: intstrPtr(intstr.FromInt(53))}, + {}, + }}, + } + } + return policy +} + +func protocolPtr(protocol corev1.Protocol) *corev1.Protocol { + return &protocol +} + +func intstrPtr(value intstr.IntOrString) *intstr.IntOrString { + return &value +} + +func testDatadogAgentObject() *v2alpha1.DatadogAgent { + return &v2alpha1.DatadogAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "datadog", + Namespace: "agents", + }, + } +} diff --git a/internal/controller/datadogagent/component/otelagentgateway/default_test.go b/internal/controller/datadogagent/component/otelagentgateway/default_test.go new file mode 100644 index 0000000000..264b23bd67 --- /dev/null +++ b/internal/controller/datadogagent/component/otelagentgateway/default_test.go @@ -0,0 +1,40 @@ +// 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 otelagentgateway + +import ( + "testing" + + "github.com/stretchr/testify/require" + 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/pkg/constants" + "github.com/DataDog/datadog-operator/pkg/kubernetes" +) + +func TestNewDefaultOtelAgentGatewayDeployment(t *testing.T) { + dda := &v2alpha1.DatadogAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "datadog", + Namespace: "agents", + }, + } + + deployment := NewDefaultOtelAgentGatewayDeployment(dda, &dda.Spec) + + require.Equal(t, "datadog-otel-agent-gateway", deployment.Name) + require.Equal(t, "agents", deployment.Namespace) + require.Equal(t, int32(defaultOtelAgentGatewayReplicas), *deployment.Spec.Replicas) + require.Equal(t, constants.DefaultOtelAgentGatewayResourceSuffix, deployment.Labels[apicommon.AgentDeploymentComponentLabelKey]) + require.Equal(t, "datadog-otel-agent-gateway", deployment.Spec.Selector.MatchLabels[kubernetes.AppKubernetesInstanceLabelKey]) + require.Equal(t, deployment.Labels, deployment.Spec.Template.Labels) + require.Equal(t, deployment.Annotations, deployment.Spec.Template.Annotations) + require.Equal(t, GetOtelAgentGatewayRbacResourcesName(dda), deployment.Spec.Template.Spec.ServiceAccountName) + require.Len(t, deployment.Spec.Template.Spec.Containers, 1) + require.Equal(t, string(apicommon.OtelAgent), deployment.Spec.Template.Spec.Containers[0].Name) +} diff --git a/internal/controller/datadogagent/feature/utils/utils_test.go b/internal/controller/datadogagent/feature/utils/utils_test.go new file mode 100644 index 0000000000..e3b839cf04 --- /dev/null +++ b/internal/controller/datadogagent/feature/utils/utils_test.go @@ -0,0 +1,122 @@ +// 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 utils + +import ( + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" +) + +func TestShouldRunProcessChecksInCoreAgent(t *testing.T) { + tests := []struct { + name string + tag string + want bool + }{ + {name: "agent before minimum version is unsupported", tag: "7.59.0", want: false}, + {name: "agent at minimum version is supported", tag: "7.60.0", want: true}, + {name: "agent after minimum version is supported", tag: "7.61.0", want: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + spec := datadogAgentSpecWithNodeAgentTag(tt.tag) + + require.Equal(t, tt.want, ShouldRunProcessChecksInCoreAgent(spec)) + }) + } +} + +func TestFeatureAnnotations(t *testing.T) { + dda := &metav1.ObjectMeta{ + Annotations: map[string]string{ + EnableADPAnnotation: "true", + EnableHostProfilerAnnotation: "false", + PrivateActionRunnerConfigDataAnnotation: "config-data", + }, + } + + require.True(t, HasFeatureEnableAnnotation(dda, EnableADPAnnotation)) + require.False(t, HasFeatureEnableAnnotation(dda, EnableHostProfilerAnnotation)) + require.False(t, HasFeatureEnableAnnotation(dda, EnableFlightRecorderAnnotation)) + + value, ok := GetFeatureConfigAnnotation(dda, PrivateActionRunnerConfigDataAnnotation) + require.True(t, ok) + require.Equal(t, "config-data", value) + + _, ok = GetFeatureConfigAnnotation(dda, ClusterAgentPrivateActionRunnerConfigDataAnnotation) + require.False(t, ok) +} + +func TestAgentSupportsADPDogstatsdDelegation(t *testing.T) { + tests := []struct { + name string + tag string + want bool + }{ + {name: "agent before minimum version needs operator delegation", tag: "7.74.0", want: false}, + {name: "agent at minimum version supports delegation", tag: "7.75.0", want: true}, + {name: "agent after minimum version supports delegation", tag: "7.76.0", want: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + spec := datadogAgentSpecWithNodeAgentTag(tt.tag) + + require.Equal(t, tt.want, AgentSupportsADPDogstatsdDelegation(spec)) + }) + } +} + +func TestIsDataPlaneEnabled(t *testing.T) { + t.Run("CRD setting takes precedence over annotation", func(t *testing.T) { + dda := &metav1.ObjectMeta{Annotations: map[string]string{EnableADPAnnotation: "true"}} + spec := &v2alpha1.DatadogAgentSpec{ + Features: &v2alpha1.DatadogFeatures{ + DataPlane: &v2alpha1.DataPlaneFeatureConfig{Enabled: ptr.To(false)}, + }, + } + + require.False(t, IsDataPlaneEnabled(dda, spec)) + }) + + t.Run("annotation enables data plane when CRD setting is omitted", func(t *testing.T) { + dda := &metav1.ObjectMeta{Annotations: map[string]string{EnableADPAnnotation: "true"}} + + require.True(t, IsDataPlaneEnabled(dda, &v2alpha1.DatadogAgentSpec{})) + }) + + t.Run("defaults to disabled", func(t *testing.T) { + require.False(t, IsDataPlaneEnabled(&metav1.ObjectMeta{}, &v2alpha1.DatadogAgentSpec{})) + }) +} + +func TestIsDataPlaneDogstatsdEnabled(t *testing.T) { + require.True(t, IsDataPlaneDogstatsdEnabled(&v2alpha1.DatadogAgentSpec{})) + + require.False(t, IsDataPlaneDogstatsdEnabled(&v2alpha1.DatadogAgentSpec{ + Features: &v2alpha1.DatadogFeatures{ + DataPlane: &v2alpha1.DataPlaneFeatureConfig{ + Dogstatsd: &v2alpha1.DataPlaneDogstatsdConfig{Enabled: ptr.To(false)}, + }, + }, + })) +} + +func datadogAgentSpecWithNodeAgentTag(tag string) *v2alpha1.DatadogAgentSpec { + return &v2alpha1.DatadogAgentSpec{ + Override: map[v2alpha1.ComponentName]*v2alpha1.DatadogAgentComponentOverride{ + v2alpha1.NodeAgentComponentName: { + Image: &v2alpha1.AgentImageConfig{Tag: tag}, + }, + }, + } +} diff --git a/internal/controller/datadogagent/object/configmap/configmap_test.go b/internal/controller/datadogagent/object/configmap/configmap_test.go new file mode 100644 index 0000000000..018f3ca3ea --- /dev/null +++ b/internal/controller/datadogagent/object/configmap/configmap_test.go @@ -0,0 +1,72 @@ +// 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 configmap + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestBuildConfigMapConfigData(t *testing.T) { + t.Run("returns nil when config data is missing", func(t *testing.T) { + got, err := BuildConfigMapConfigData("agents", nil, "datadog-config", "datadog.yaml") + + require.NoError(t, err) + require.Nil(t, got) + }) + + t.Run("returns nil when config data is empty", func(t *testing.T) { + empty := "" + + got, err := BuildConfigMapConfigData("agents", &empty, "datadog-config", "datadog.yaml") + + require.NoError(t, err) + require.Nil(t, got) + }) + + t.Run("builds a configmap for valid yaml", func(t *testing.T) { + configData := "logs_enabled: true\n" + + got, err := BuildConfigMapConfigData("agents", &configData, "datadog-config", "datadog.yaml") + + require.NoError(t, err) + require.Equal(t, "datadog-config", got.Name) + require.Equal(t, "agents", got.Namespace) + require.Equal(t, map[string]string{"datadog.yaml": configData}, got.Data) + }) + + t.Run("returns an error for invalid yaml", func(t *testing.T) { + configData := ":\n" + + got, err := BuildConfigMapConfigData("agents", &configData, "datadog-config", "datadog.yaml") + + require.Error(t, err) + require.Nil(t, got) + }) +} + +func TestBuildConfigMapMulti(t *testing.T) { + t.Run("accepts arbitrary data when validation is disabled", func(t *testing.T) { + got, err := BuildConfigMapMulti("agents", map[string]string{ + "check.py": "def check(): pass", + }, "checks", false) + + require.NoError(t, err) + require.Equal(t, "checks", got.Name) + require.Equal(t, map[string]string{"check.py": "def check(): pass"}, got.Data) + }) + + t.Run("keeps valid yaml and reports invalid yaml when validation is enabled", func(t *testing.T) { + got, err := BuildConfigMapMulti("agents", map[string]string{ + "valid.yaml": "instances: []\n", + "bad.yaml": ":\n", + }, "checks", true) + + require.Error(t, err) + require.Equal(t, map[string]string{"valid.yaml": "instances: []\n"}, got.Data) + }) +} diff --git a/internal/controller/datadogagent/object/labels_test.go b/internal/controller/datadogagent/object/labels_test.go new file mode 100644 index 0000000000..b13cca0b5c --- /dev/null +++ b/internal/controller/datadogagent/object/labels_test.go @@ -0,0 +1,65 @@ +// 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 object + +import ( + "testing" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/DataDog/datadog-operator/pkg/kubernetes" +) + +func TestGetDefaultLabels(t *testing.T) { + dda := &metav1.ObjectMeta{ + Name: "datadog", + Namespace: "agents", + Labels: map[string]string{ + "tags.datadoghq.com/env": "prod", + "unrelated": "ignored", + }, + } + + got := GetDefaultLabels(dda, "datadog-agent", "7.78.0") + + require.Equal(t, map[string]string{ + kubernetes.AppKubernetesNameLabelKey: "datadog-agent-deployment", + kubernetes.AppKubernetesInstanceLabelKey: "datadog-agent", + kubernetes.AppKubernetesPartOfLabelKey: "agents-datadog", + kubernetes.AppKubernetesVersionLabelKey: "7.78.0", + kubernetes.AppKubernetesManageByLabelKey: "datadog-operator", + "tags.datadoghq.com/env": "prod", + }, got) +} + +func TestMergeAnnotationsLabels(t *testing.T) { + previous := map[string]string{ + "keep.datadoghq.com/value": "kept-by-domain", + "custom.keep": "kept-by-filter", + "drop": "removed", + "overwrite": "old", + } + newValues := map[string]string{ + "overwrite": "new", + "new": "value", + } + + got := MergeAnnotationsLabels(logr.Discard(), previous, newValues, "custom.*") + + require.Equal(t, map[string]string{ + "keep.datadoghq.com/value": "kept-by-domain", + "custom.keep": "kept-by-filter", + "overwrite": "new", + "new": "value", + }, got) +} + +func TestGetChecksumAnnotationKey(t *testing.T) { + require.Empty(t, GetChecksumAnnotationKey("")) + require.Equal(t, "checksum/datadog-custom-config", GetChecksumAnnotationKey("datadog")) +} diff --git a/internal/controller/datadogagent/object/owner_ref_test.go b/internal/controller/datadogagent/object/owner_ref_test.go new file mode 100644 index 0000000000..af01bb8249 --- /dev/null +++ b/internal/controller/datadogagent/object/owner_ref_test.go @@ -0,0 +1,77 @@ +// 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 object + +import ( + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + + "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" +) + +func TestSetOwnerReference(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, v2alpha1.AddToScheme(scheme)) + + owner := &v2alpha1.DatadogAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "datadog", + UID: types.UID("owner-uid"), + }, + } + child := &metav1.ObjectMeta{Name: "child"} + + require.NoError(t, SetOwnerReference(owner, child, scheme)) + require.True(t, CheckOwnerReference(owner, child)) + require.Equal(t, []metav1.OwnerReference{{ + APIVersion: v2alpha1.GroupVersion.String(), + Kind: "DatadogAgent", + Name: "datadog", + UID: types.UID("owner-uid"), + Controller: boolPtr(true), + BlockOwnerDeletion: boolPtr(true), + }}, child.OwnerReferences) + + owner.UID = types.UID("new-owner-uid") + require.NoError(t, SetOwnerReference(owner, child, scheme)) + require.Len(t, child.OwnerReferences, 1) + require.Equal(t, types.UID("new-owner-uid"), child.OwnerReferences[0].UID) +} + +func TestCreateOwnerRefRejectsNonRuntimeObject(t *testing.T) { + ref, err := CreateOwnerRef(&metav1.ObjectMeta{Name: "not-runtime"}, runtime.NewScheme()) + + require.Error(t, err) + require.Nil(t, ref) +} + +func TestReferSameObject(t *testing.T) { + base := metav1.OwnerReference{ + APIVersion: "datadoghq.com/v2alpha1", + Kind: "DatadogAgent", + Name: "datadog", + } + + require.True(t, referSameObject(base, base)) + require.False(t, referSameObject(base, metav1.OwnerReference{ + APIVersion: "datadoghq.com/v2alpha1", + Kind: "DatadogAgent", + Name: "other", + })) + require.False(t, referSameObject(base, metav1.OwnerReference{ + APIVersion: "not a group version", + Kind: "DatadogAgent", + Name: "datadog", + })) +} + +func boolPtr(value bool) *bool { + return &value +} diff --git a/internal/controller/datadogagent/object/volume/volumes_test.go b/internal/controller/datadogagent/object/volume/volumes_test.go new file mode 100644 index 0000000000..2681a55f6c --- /dev/null +++ b/internal/controller/datadogagent/object/volume/volumes_test.go @@ -0,0 +1,120 @@ +// 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 volume + +import ( + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + + "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" + "github.com/DataDog/datadog-operator/internal/controller/datadogagent/common" +) + +func TestGetVolumes(t *testing.T) { + vol, mount := GetVolumes("logs", "/var/log/pods", "/host/var/log/pods", true) + + require.Equal(t, "logs", vol.Name) + require.NotNil(t, vol.HostPath) + require.Equal(t, "/var/log/pods", vol.HostPath.Path) + require.Equal(t, corev1.VolumeMount{ + Name: "logs", + MountPath: "/host/var/log/pods", + ReadOnly: true, + }, mount) +} + +func TestGetVolumesEmptyDir(t *testing.T) { + vol, mount := GetVolumesEmptyDir("tmp", "/tmp", false) + + require.Equal(t, "tmp", vol.Name) + require.NotNil(t, vol.EmptyDir) + require.Equal(t, "tmp", mount.Name) + require.Equal(t, "/tmp", mount.MountPath) + require.False(t, mount.ReadOnly) +} + +func TestGetVolumesFromConfigMap(t *testing.T) { + configMap := &v2alpha1.ConfigMapConfig{ + Name: "custom-checks", + Items: []corev1.KeyToPath{ + {Key: "redis.yaml", Path: "redis.yaml"}, + }, + } + + vol, mount := GetVolumesFromConfigMap(configMap, "checks", "default-checks", "redisdb") + + require.Equal(t, "checks", vol.Name) + require.Equal(t, "custom-checks", vol.ConfigMap.Name) + require.Equal(t, []corev1.KeyToPath{{Key: "redis.yaml", Path: "redis.yaml"}}, vol.ConfigMap.Items) + require.Equal(t, "checks", mount.Name) + require.Equal(t, common.ConfigVolumePath+common.ConfdVolumePath+"/redisdb", mount.MountPath) + require.True(t, mount.ReadOnly) +} + +func TestGetVolumeFromCustomConfig(t *testing.T) { + configData := "logs_enabled: true" + + t.Run("uses referenced configmap when present", func(t *testing.T) { + vol := GetVolumeFromCustomConfig(v2alpha1.CustomConfig{ + ConfigMap: &v2alpha1.ConfigMapConfig{Name: "custom"}, + }, "default", "config") + + require.Equal(t, "custom", vol.ConfigMap.Name) + }) + + t.Run("uses generated configmap when config data is present", func(t *testing.T) { + vol := GetVolumeFromCustomConfig(v2alpha1.CustomConfig{ + ConfigData: &configData, + }, "generated", "config") + + require.Equal(t, "generated", vol.ConfigMap.Name) + }) + + t.Run("returns an empty volume when no custom config is set", func(t *testing.T) { + require.Empty(t, GetVolumeFromCustomConfig(v2alpha1.CustomConfig{}, "generated", "config")) + }) +} + +func TestGetVolumeFromMultiCustomConfig(t *testing.T) { + t.Run("uses referenced configmap items", func(t *testing.T) { + vol := GetVolumeFromMultiCustomConfig(&v2alpha1.MultiCustomConfig{ + ConfigMap: &v2alpha1.ConfigMapConfig{ + Name: "custom-confd", + Items: []corev1.KeyToPath{{Key: "check.yaml", Path: "check.yaml"}}, + }, + }, "confd", "generated-confd") + + require.Equal(t, "custom-confd", vol.ConfigMap.Name) + require.Equal(t, []corev1.KeyToPath{{Key: "check.yaml", Path: "check.yaml"}}, vol.ConfigMap.Items) + }) + + t.Run("sorts valid config data keys and skips invalid yaml", func(t *testing.T) { + vol := GetVolumeFromMultiCustomConfig(&v2alpha1.MultiCustomConfig{ + ConfigDataMap: map[string]string{ + "z.yaml": "init_config: {}\n", + "bad": ":\n", + "a.yaml": "instances: []\n", + }, + }, "confd", "generated-confd") + + require.Equal(t, "generated-confd", vol.ConfigMap.Name) + require.Equal(t, []corev1.KeyToPath{ + {Key: "a.yaml", Path: "a.yaml"}, + {Key: "z.yaml", Path: "z.yaml"}, + }, vol.ConfigMap.Items) + }) +} + +func TestGetVolumeMountWithSubPath(t *testing.T) { + require.Equal(t, corev1.VolumeMount{ + Name: "config", + MountPath: "/etc/datadog-agent/datadog.yaml", + SubPath: "datadog.yaml", + ReadOnly: true, + }, GetVolumeMountWithSubPath("config", "/etc/datadog-agent/datadog.yaml", "datadog.yaml")) +} diff --git a/pkg/condition/status_test.go b/pkg/condition/status_test.go new file mode 100644 index 0000000000..370ba06ccf --- /dev/null +++ b/pkg/condition/status_test.go @@ -0,0 +1,257 @@ +// 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 condition + +import ( + "testing" + "time" + + edsdatadoghqv1alpha1 "github.com/DataDog/extendeddaemonset/api/v1alpha1" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" + "github.com/DataDog/datadog-operator/pkg/constants" +) + +func TestUpdateDeploymentStatus(t *testing.T) { + now := metav1.NewTime(time.Date(2026, 5, 15, 12, 0, 0, 0, time.UTC)) + + tests := []struct { + name string + deploy *appsv1.Deployment + wantState string + }{ + { + name: "missing deployment is failed", + deploy: nil, + wantState: string(DatadogAgentStateFailed), + }, + { + name: "replica failure condition is failed", + deploy: deploymentWithStatus("agent", appsv1.DeploymentStatus{ + Replicas: 3, + UpdatedReplicas: 3, + ReadyReplicas: 3, + Conditions: []appsv1.DeploymentCondition{ + {Type: appsv1.DeploymentReplicaFailure, Status: corev1.ConditionTrue}, + }, + }), + wantState: string(DatadogAgentStateFailed), + }, + { + name: "updated replicas behind desired replicas is updating", + deploy: deploymentWithStatus("agent", appsv1.DeploymentStatus{ + Replicas: 3, + UpdatedReplicas: 2, + ReadyReplicas: 2, + }), + wantState: string(DatadogAgentStateUpdating), + }, + { + name: "no ready replicas is progressing", + deploy: deploymentWithStatus("agent", appsv1.DeploymentStatus{ + Replicas: 3, + UpdatedReplicas: 3, + ReadyReplicas: 0, + }), + wantState: string(DatadogAgentStateProgressing), + }, + { + name: "ready deployment is running", + deploy: deploymentWithStatus("agent", appsv1.DeploymentStatus{ + Replicas: 3, + UpdatedReplicas: 3, + ReadyReplicas: 3, + }), + wantState: string(DatadogAgentStateRunning), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := UpdateDeploymentStatus(tt.deploy, nil, &now) + + require.Equal(t, tt.wantState, got.State) + }) + } +} + +func TestUpdateDaemonSetStatusStates(t *testing.T) { + tests := []struct { + name string + status appsv1.DaemonSetStatus + wantState string + }{ + { + name: "updated nodes behind desired nodes is updating", + status: appsv1.DaemonSetStatus{ + DesiredNumberScheduled: 3, + UpdatedNumberScheduled: 2, + NumberReady: 2, + }, + wantState: string(DatadogAgentStateUpdating), + }, + { + name: "desired nodes with no ready nodes is progressing", + status: appsv1.DaemonSetStatus{ + DesiredNumberScheduled: 3, + UpdatedNumberScheduled: 3, + NumberReady: 0, + }, + wantState: string(DatadogAgentStateProgressing), + }, + { + name: "up-to-date ready nodes are running", + status: appsv1.DaemonSetStatus{ + DesiredNumberScheduled: 3, + UpdatedNumberScheduled: 3, + NumberReady: 3, + }, + wantState: string(DatadogAgentStateRunning), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ds := &appsv1.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "agent", + Annotations: map[string]string{ + constants.MD5AgentDeploymentAnnotationKey: "hash", + }, + }, + Status: tt.status, + } + + got := UpdateDaemonSetStatus("agent", ds, nil, nil) + + require.Len(t, got, 1) + require.Equal(t, tt.wantState, got[0].State) + require.Equal(t, "hash", got[0].CurrentHash) + }) + } +} + +func TestUpdateExtendedDaemonSetStatusStates(t *testing.T) { + tests := []struct { + name string + status edsdatadoghqv1alpha1.ExtendedDaemonSetStatus + wantState string + }{ + { + name: "canary status has priority", + status: edsdatadoghqv1alpha1.ExtendedDaemonSetStatus{ + Desired: 3, + Ready: 3, + UpToDate: 3, + Canary: &edsdatadoghqv1alpha1.ExtendedDaemonSetStatusCanary{}, + }, + wantState: string(DatadogAgentStateCanary), + }, + { + name: "updated nodes behind desired nodes is updating", + status: edsdatadoghqv1alpha1.ExtendedDaemonSetStatus{ + Desired: 3, + Ready: 2, + UpToDate: 2, + }, + wantState: string(DatadogAgentStateUpdating), + }, + { + name: "desired nodes with no ready nodes is progressing", + status: edsdatadoghqv1alpha1.ExtendedDaemonSetStatus{ + Desired: 3, + Ready: 0, + UpToDate: 3, + }, + wantState: string(DatadogAgentStateProgressing), + }, + { + name: "up-to-date ready nodes are running", + status: edsdatadoghqv1alpha1.ExtendedDaemonSetStatus{ + Desired: 3, + Ready: 3, + UpToDate: 3, + }, + wantState: string(DatadogAgentStateRunning), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + eds := &edsdatadoghqv1alpha1.ExtendedDaemonSet{ + ObjectMeta: metav1.ObjectMeta{Name: "agent"}, + Status: tt.status, + } + + got := UpdateExtendedDaemonSetStatus(eds, nil, nil) + + require.Len(t, got, 1) + require.Equal(t, tt.wantState, got[0].State) + }) + } +} + +func TestUpdateCombinedDaemonSetStatus(t *testing.T) { + got := UpdateCombinedDaemonSetStatus([]*v2alpha1.DaemonSetStatus{ + { + Desired: 2, + Current: 2, + Ready: 2, + Available: 2, + UpToDate: 2, + State: string(DatadogAgentStateRunning), + }, + { + Desired: 1, + Current: 1, + Ready: 0, + Available: 0, + UpToDate: 0, + State: string(DatadogAgentStateUpdating), + }, + }) + + require.Equal(t, int32(3), got.Desired) + require.Equal(t, int32(2), got.Ready) + require.Equal(t, int32(2), got.UpToDate) + require.Equal(t, string(DatadogAgentStateUpdating), got.State) + require.Equal(t, "Updating (3/2/2)", got.Status) +} + +func TestIsEqualConditions(t *testing.T) { + current := []metav1.Condition{ + {Type: "Ready", Status: metav1.ConditionTrue, Reason: "Ready", Message: "ready"}, + {Type: "Valid", Status: metav1.ConditionFalse, Reason: "Invalid", Message: "invalid"}, + } + sameDifferentOrder := []metav1.Condition{ + {Type: "Valid", Status: metav1.ConditionFalse, Reason: "Invalid", Message: "invalid"}, + {Type: "Ready", Status: metav1.ConditionTrue, Reason: "Ready", Message: "ready"}, + } + differentMessage := []metav1.Condition{ + {Type: "Ready", Status: metav1.ConditionTrue, Reason: "Ready", Message: "changed"}, + {Type: "Valid", Status: metav1.ConditionFalse, Reason: "Invalid", Message: "invalid"}, + } + + require.True(t, IsEqualConditions(current, sameDifferentOrder)) + require.False(t, IsEqualConditions(current, differentMessage)) + require.False(t, IsEqualConditions(current, current[:1])) +} + +func deploymentWithStatus(name string, status appsv1.DeploymentStatus) *appsv1.Deployment { + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Annotations: map[string]string{ + constants.MD5AgentDeploymentAnnotationKey: "hash", + }, + }, + Status: status, + } +} diff --git a/pkg/constants/utils_test.go b/pkg/constants/utils_test.go index 16d1f673e1..cce0f44ec6 100644 --- a/pkg/constants/utils_test.go +++ b/pkg/constants/utils_test.go @@ -8,6 +8,10 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" + "k8s.io/utils/ptr" + + apicommon "github.com/DataDog/datadog-operator/api/datadoghq/common" "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -59,3 +63,128 @@ func TestServiceAccountNameOverride(t *testing.T) { }) } } + +func TestGetConfName(t *testing.T) { + dda := &v1.ObjectMeta{Name: "datadog"} + configData := "logs_enabled: true" + + require.Equal(t, "custom-config", GetConfName(dda, &v2alpha1.CustomConfig{ + ConfigMap: &v2alpha1.ConfigMapConfig{Name: "custom-config"}, + }, "datadog-config")) + require.Equal(t, "datadog-datadog-config", GetConfName(dda, &v2alpha1.CustomConfig{ + ConfigData: &configData, + }, "datadog-config")) + require.Equal(t, "datadog-datadog-config", GetConfName(dda, nil, "datadog-config")) +} + +func TestGetServiceAccountByComponent(t *testing.T) { + ddaSpec := &v2alpha1.DatadogAgentSpec{Override: map[v2alpha1.ComponentName]*v2alpha1.DatadogAgentComponentOverride{}} + + require.Equal(t, "datadog-agent", GetServiceAccountByComponent("datadog", ddaSpec, v2alpha1.NodeAgentComponentName)) + require.Equal(t, "datadog-cluster-agent", GetServiceAccountByComponent("datadog", ddaSpec, v2alpha1.ClusterAgentComponentName)) + require.Equal(t, "datadog-cluster-checks-runner", GetServiceAccountByComponent("datadog", ddaSpec, v2alpha1.ClusterChecksRunnerComponentName)) + require.Empty(t, GetServiceAccountByComponent("datadog", ddaSpec, v2alpha1.ComponentName("unknown"))) +} + +func TestGetOtelAgentGatewayServiceAccount(t *testing.T) { + customServiceAccount := "custom-otel-sa" + ddaSpec := &v2alpha1.DatadogAgentSpec{ + Override: map[v2alpha1.ComponentName]*v2alpha1.DatadogAgentComponentOverride{ + v2alpha1.OtelAgentGatewayComponentName: { + ServiceAccountName: &customServiceAccount, + }, + }, + } + + require.Equal(t, customServiceAccount, GetOtelAgentGatewayServiceAccount("datadog", ddaSpec)) + delete(ddaSpec.Override, v2alpha1.OtelAgentGatewayComponentName) + require.Equal(t, "datadog-otel-agent-gateway", GetOtelAgentGatewayServiceAccount("datadog", ddaSpec)) +} + +func TestIsHostNetworkEnabled(t *testing.T) { + require.False(t, IsHostNetworkEnabled(&v2alpha1.DatadogAgentSpec{}, v2alpha1.NodeAgentComponentName)) + require.False(t, IsHostNetworkEnabled(&v2alpha1.DatadogAgentSpec{ + Override: map[v2alpha1.ComponentName]*v2alpha1.DatadogAgentComponentOverride{ + v2alpha1.NodeAgentComponentName: {}, + }, + }, v2alpha1.NodeAgentComponentName)) + require.True(t, IsHostNetworkEnabled(&v2alpha1.DatadogAgentSpec{ + Override: map[v2alpha1.ComponentName]*v2alpha1.DatadogAgentComponentOverride{ + v2alpha1.NodeAgentComponentName: {HostNetwork: ptr.To(true)}, + }, + }, v2alpha1.NodeAgentComponentName)) +} + +func TestClusterChecksFlags(t *testing.T) { + ddaSpec := &v2alpha1.DatadogAgentSpec{ + Features: &v2alpha1.DatadogFeatures{ + ClusterChecks: &v2alpha1.ClusterChecksFeatureConfig{ + Enabled: ptr.To(true), + UseClusterChecksRunners: ptr.To(true), + }, + }, + } + + require.True(t, IsClusterChecksEnabled(ddaSpec)) + require.True(t, IsCCREnabled(ddaSpec)) + + ddaSpec.Features.ClusterChecks.Enabled = ptr.To(false) + ddaSpec.Features.ClusterChecks.UseClusterChecksRunners = ptr.To(false) + require.False(t, IsClusterChecksEnabled(ddaSpec)) + require.False(t, IsCCREnabled(ddaSpec)) +} + +func TestServiceNames(t *testing.T) { + customLocalServiceName := "custom-local-agent" + + require.Equal(t, customLocalServiceName, GetLocalAgentServiceName("datadog", &v2alpha1.DatadogAgentSpec{ + Global: &v2alpha1.GlobalConfig{ + LocalService: &v2alpha1.LocalService{NameOverride: &customLocalServiceName}, + }, + })) + require.Equal(t, "datadog-agent", GetLocalAgentServiceName("datadog", &v2alpha1.DatadogAgentSpec{ + Global: &v2alpha1.GlobalConfig{}, + })) + require.Equal(t, "datadog-otel-agent-gateway", GetOTelAgentGatewayServiceName("datadog")) +} + +func TestIsNetworkPolicyEnabled(t *testing.T) { + enabled, flavor := IsNetworkPolicyEnabled(&v2alpha1.DatadogAgentSpec{}) + require.False(t, enabled) + require.Empty(t, flavor) + + enabled, flavor = IsNetworkPolicyEnabled(&v2alpha1.DatadogAgentSpec{ + Global: &v2alpha1.GlobalConfig{ + NetworkPolicy: &v2alpha1.NetworkPolicyConfig{Create: ptr.To(true)}, + }, + }) + require.True(t, enabled) + require.Equal(t, v2alpha1.NetworkPolicyFlavorKubernetes, flavor) + + enabled, flavor = IsNetworkPolicyEnabled(&v2alpha1.DatadogAgentSpec{ + Global: &v2alpha1.GlobalConfig{ + NetworkPolicy: &v2alpha1.NetworkPolicyConfig{ + Create: ptr.To(true), + Flavor: v2alpha1.NetworkPolicyFlavorCilium, + }, + }, + }) + require.True(t, enabled) + require.Equal(t, v2alpha1.NetworkPolicyFlavorCilium, flavor) +} + +func TestGetDDAName(t *testing.T) { + require.Equal(t, "from-label", GetDDAName(&v1.ObjectMeta{ + Name: "object-name", + Labels: map[string]string{ + apicommon.DatadogAgentNameLabelKey: "from-label", + }, + })) + require.Equal(t, "object-name", GetDDAName(&v1.ObjectMeta{ + Name: "object-name", + Labels: map[string]string{ + apicommon.DatadogAgentNameLabelKey: "", + }, + })) + require.Equal(t, "object-name", GetDDAName(&v1.ObjectMeta{Name: "object-name"})) +} diff --git a/pkg/controller/utils/comparison/comparison_test.go b/pkg/controller/utils/comparison/comparison_test.go new file mode 100644 index 0000000000..bf3fea4663 --- /dev/null +++ b/pkg/controller/utils/comparison/comparison_test.go @@ -0,0 +1,64 @@ +// 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 comparison + +import ( + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/DataDog/datadog-operator/pkg/constants" +) + +func TestGenerateMD5ForSpec(t *testing.T) { + t.Run("returns the same hash for the same spec", func(t *testing.T) { + spec := map[string]any{ + "agent": map[string]any{ + "enabled": true, + }, + } + + firstHash, err := GenerateMD5ForSpec(spec) + require.NoError(t, err) + require.NotEmpty(t, firstHash) + + secondHash, err := GenerateMD5ForSpec(spec) + require.NoError(t, err) + require.Equal(t, firstHash, secondHash) + }) + + t.Run("returns a marshal error for unsupported values", func(t *testing.T) { + _, err := GenerateMD5ForSpec(map[string]any{ + "unsupported": make(chan struct{}), + }) + require.Error(t, err) + }) +} + +func TestSetMD5GenerationAnnotation(t *testing.T) { + t.Run("initializes annotations and writes the generated hash", func(t *testing.T) { + obj := &metav1.ObjectMeta{} + spec := map[string]bool{"enabled": true} + + hash, err := SetMD5GenerationAnnotation(obj, spec, "custom-hash") + require.NoError(t, err) + + require.Equal(t, hash, obj.Annotations["custom-hash"]) + require.True(t, IsSameMD5Hash(hash, obj.Annotations, "custom-hash")) + require.False(t, IsSameMD5Hash("different-hash", obj.Annotations, "custom-hash")) + }) + + t.Run("uses the DatadogAgent annotation helper", func(t *testing.T) { + obj := &metav1.ObjectMeta{Annotations: map[string]string{}} + + hash, err := SetMD5DatadogAgentGenerationAnnotation(obj, map[string]string{"image": "agent"}) + require.NoError(t, err) + + require.Equal(t, hash, obj.Annotations[constants.MD5AgentDeploymentAnnotationKey]) + require.True(t, IsSameSpecMD5Hash(hash, obj.Annotations)) + }) +} diff --git a/pkg/controller/utils/shared_utils_test.go b/pkg/controller/utils/shared_utils_test.go new file mode 100644 index 0000000000..bcc09d1ee4 --- /dev/null +++ b/pkg/controller/utils/shared_utils_test.go @@ -0,0 +1,61 @@ +// 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 utils + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" +) + +func TestShouldReturn(t *testing.T) { + tests := []struct { + name string + result reconcile.Result + err error + want bool + }{ + {name: "keeps reconciling when result and error are empty", want: false}, + {name: "returns on error", err: errors.New("boom"), want: true}, + {name: "returns on non-empty result", result: reconcile.Result{Requeue: true}, want: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, ShouldReturn(tt.result, tt.err)) + }) + } +} + +func TestUseCustomSeccompConfig(t *testing.T) { + configData := "{}" + + require.False(t, UseCustomSeccompConfigMap(nil)) + require.False(t, UseCustomSeccompConfigData(nil)) + + require.True(t, UseCustomSeccompConfigMap(&v2alpha1.SeccompConfig{ + CustomProfile: &v2alpha1.CustomConfig{ + ConfigMap: &v2alpha1.ConfigMapConfig{Name: "profile"}, + }, + })) + require.False(t, UseCustomSeccompConfigData(&v2alpha1.SeccompConfig{ + CustomProfile: &v2alpha1.CustomConfig{ + ConfigMap: &v2alpha1.ConfigMapConfig{Name: "profile"}, + ConfigData: ptr.To(configData), + }, + })) + + require.True(t, UseCustomSeccompConfigData(&v2alpha1.SeccompConfig{ + CustomProfile: &v2alpha1.CustomConfig{ + ConfigData: ptr.To(configData), + }, + })) +}