Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion internal/controller/datadogagent/experimental/autopilot.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,12 @@ func IsAutopilotEnabled(obj metav1.Object) bool {
return strings.EqualFold(ann[getExperimentalAnnotationKey(ExperimentalAutopilotSubkey)], "true")
}

// createAllowlistSynchronizer is overridden in tests to avoid hitting kubeconfig loading.
var createAllowlistSynchronizer = allowlistsynchronizer.CreateAllowlistSynchronizer

func applyExperimentalAutopilotOverrides(dda metav1.Object, manager feature.PodTemplateManagers) {
if IsAutopilotEnabled(dda) {
allowlistsynchronizer.CreateAllowlistSynchronizer()
createAllowlistSynchronizer(hasOtelAgentContainer(manager))

if manager.PodTemplateSpec().Labels == nil {
manager.PodTemplateSpec().Labels = map[string]string{}
Expand Down Expand Up @@ -151,3 +154,15 @@ func applyExperimentalAutopilotOverrides(dda metav1.Object, manager feature.PodT
}
}
}

// hasOtelAgentContainer reports whether the otel-agent (ddot-collector) container is
// present in the PodTemplateSpec, which signals that the OTel collector feature is
// enabled on the DatadogAgent and the v1.0.5 WorkloadAllowlist exemption is required.
func hasOtelAgentContainer(manager feature.PodTemplateManagers) bool {
for _, c := range manager.PodTemplateSpec().Spec.Containers {
if c.Name == string(apicommon.OtelAgent) {
return true
}
}
return false
}
124 changes: 124 additions & 0 deletions internal/controller/datadogagent/experimental/autopilot_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// 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"

apicommon "github.com/DataDog/datadog-operator/api/datadoghq/common"
"github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/fake"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// TestHasOtelAgentContainer verifies that the otel-agent container detection used to
// gate the v1.0.5 WorkloadAllowlist exemption inclusion works for both the present
// and absent cases. See OTAGENT-980.
func TestHasOtelAgentContainer(t *testing.T) {
tests := []struct {
name string
containers []corev1.Container
expected bool
}{
{
name: "no otel-agent container",
containers: []corev1.Container{
{Name: string(apicommon.CoreAgentContainerName)},
{Name: string(apicommon.TraceAgentContainerName)},
},
expected: false,
},
{
name: "otel-agent container present",
containers: []corev1.Container{
{Name: string(apicommon.CoreAgentContainerName)},
{Name: string(apicommon.OtelAgent)},
},
expected: true,
},
{
name: "empty containers",
containers: nil,
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
manager := fake.NewPodTemplateManagers(t, corev1.PodTemplateSpec{
Spec: corev1.PodSpec{Containers: tt.containers},
})
assert.Equal(t, tt.expected, hasOtelAgentContainer(manager))
})
}
}

// TestApplyExperimentalAutopilotOverrides_SynchronizerInvocation verifies that the
// autopilot override branch invokes the synchronizer creator with the correct flag
// derived from the rendered pod template (OTAGENT-980).
func TestApplyExperimentalAutopilotOverrides_SynchronizerInvocation(t *testing.T) {
// Stub out the synchronizer creator so tests don't hit kubeconfig loading.
origCreator := createAllowlistSynchronizer
defer func() { createAllowlistSynchronizer = origCreator }()

autopilotAnnotation := map[string]string{
ExperimentalAnnotationPrefix + "/" + ExperimentalAutopilotSubkey: "true",
}

tests := []struct {
name string
annotations map[string]string
containers []corev1.Container
expectInvoked bool
expectOtelFlagPassed bool
}{
{
name: "autopilot disabled does not invoke synchronizer",
annotations: nil,
containers: []corev1.Container{{Name: string(apicommon.CoreAgentContainerName)}},
expectInvoked: false,
expectOtelFlagPassed: false,
},
{
name: "autopilot enabled without otel-agent invokes synchronizer with false",
annotations: autopilotAnnotation,
containers: []corev1.Container{{Name: string(apicommon.CoreAgentContainerName)}},
expectInvoked: true,
expectOtelFlagPassed: false,
},
{
name: "autopilot enabled with otel-agent invokes synchronizer with true",
annotations: autopilotAnnotation,
containers: []corev1.Container{{Name: string(apicommon.CoreAgentContainerName)}, {Name: string(apicommon.OtelAgent)}},
expectInvoked: true,
expectOtelFlagPassed: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var invoked bool
var passedFlag bool
createAllowlistSynchronizer = func(otelCollectorEnabled bool) {
invoked = true
passedFlag = otelCollectorEnabled
}

dda := &metav1.ObjectMeta{Annotations: tt.annotations}
manager := fake.NewPodTemplateManagers(t, corev1.PodTemplateSpec{
Spec: corev1.PodSpec{Containers: tt.containers},
})

applyExperimentalAutopilotOverrides(dda, manager)

assert.Equal(t, tt.expectInvoked, invoked, "synchronizer creator invocation")
if tt.expectInvoked {
assert.Equal(t, tt.expectOtelFlagPassed, passedFlag, "otelCollectorEnabled flag")
}
})
}
}
71 changes: 60 additions & 11 deletions pkg/allowlistsynchronizer/allowlistsynchronizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package allowlistsynchronizer

import (
"context"
"slices"

apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -29,6 +30,16 @@ var (

var logger = logf.Log.WithName("AllowlistSynchronizer")

// Allowlist paths kept in sync with the Datadog partner exemption YAMLs published in
// the datadog-gke-workload-allowlist repo.
const (
// allowlistPathV101 exempts the base agent / process-agent / trace-agent containers.
allowlistPathV101 = "Datadog/datadog/datadog-datadog-daemonset-exemption-v1.0.1.yaml"
// allowlistPathV105 exempts the otel-agent (ddot-collector) container when OTel
// feature gates are configured. See OTAGENT-980.
allowlistPathV105 = "Datadog/datadog/datadog-datadog-daemonset-exemption-v1.0.5.yaml"
)

type AllowlistSynchronizer struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata"`
Expand All @@ -46,8 +57,21 @@ type AllowlistSynchronizerSpec struct {
AllowlistPaths []string `json:"allowlistPaths,omitempty"`
}

func createAllowlistSynchronizerResource(k8sClient client.Client) error {
obj := &AllowlistSynchronizer{
// ComputeAllowlistPaths returns the set of Datadog WorkloadAllowlist exemption paths
// that the AllowlistSynchronizer should reference for the given DatadogAgent.
//
// v1.0.5 is included when the OTel collector is enabled so that the otel-agent
// (ddot-collector image) container is permitted by GKE Autopilot Warden. See OTAGENT-980.
func ComputeAllowlistPaths(otelCollectorEnabled bool) []string {
paths := []string{allowlistPathV101}
if otelCollectorEnabled {
paths = append(paths, allowlistPathV105)
}
return paths
}

func newAllowlistSynchronizer(paths []string) *AllowlistSynchronizer {
return &AllowlistSynchronizer{
TypeMeta: metav1.TypeMeta{
APIVersion: "allowlistsynchronizers.auto.gke.io",
Kind: "AllowlistSynchronizer",
Expand All @@ -60,19 +84,22 @@ func createAllowlistSynchronizerResource(k8sClient client.Client) error {
},
},
Spec: AllowlistSynchronizerSpec{
AllowlistPaths: []string{
"Datadog/datadog/datadog-datadog-daemonset-exemption-v1.0.1.yaml",
},
AllowlistPaths: paths,
},
}
}

return k8sClient.Create(context.TODO(), obj)
func createAllowlistSynchronizerResource(k8sClient client.Client, paths []string) error {
return k8sClient.Create(context.TODO(), newAllowlistSynchronizer(paths))
}

// 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() {
//
// otelCollectorEnabled controls whether the v1.0.5 exemption path (which permits the
// otel-agent / ddot-collector container) is included.
func CreateAllowlistSynchronizer(otelCollectorEnabled bool) {
cfg, configErr := config.GetConfig()
if configErr != nil {
logger.Error(configErr, "failed to load kubeconfig")
Expand All @@ -91,15 +118,37 @@ func CreateAllowlistSynchronizer() {
return
}

reconcileAllowlistSynchronizer(k8sClient, otelCollectorEnabled)
}

// reconcileAllowlistSynchronizer creates the AllowlistSynchronizer resource if it does
// not already exist; if it exists with stale allowlistPaths (e.g. installed by an older
// operator version that didn't reference v1.0.5), the spec is updated in place so OTel
// collector workloads are admitted by Warden after enabling the feature. Extracted from
// CreateAllowlistSynchronizer so it can be exercised with a fake client.
func reconcileAllowlistSynchronizer(k8sClient client.Client, otelCollectorEnabled bool) {
desired := ComputeAllowlistPaths(otelCollectorEnabled)

existing := &AllowlistSynchronizer{}
if existingErr := k8sClient.Get(context.TODO(), client.ObjectKey{Name: "datadog-synchronizer"}, existing); existingErr == nil {
getErr := k8sClient.Get(context.TODO(), client.ObjectKey{Name: "datadog-synchronizer"}, existing)
switch {
case getErr == nil:
if slices.Equal(existing.Spec.AllowlistPaths, desired) {
return
}
existing.Spec.AllowlistPaths = desired
if updateErr := k8sClient.Update(context.TODO(), existing); updateErr != nil {
logger.Error(updateErr, "failed to update AllowlistSynchronizer resource")
return
}
logger.Info("Successfully updated AllowlistSynchronizer allowlistPaths")
return
} else if !apierrors.IsNotFound(existingErr) {
logger.Error(existingErr, "failed to check existing AllowlistSynchronizer resource")
case !apierrors.IsNotFound(getErr):
logger.Error(getErr, "failed to check existing AllowlistSynchronizer resource")
return
}

if err := createAllowlistSynchronizerResource(k8sClient); err != nil {
if err := createAllowlistSynchronizerResource(k8sClient, desired); err != nil {
if apierrors.IsAlreadyExists(err) {
return
}
Expand Down
Loading
Loading