Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
datadoghqv1alpha1 "github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1"
datadoghqv2alpha1 "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/component"
"github.com/DataDog/datadog-operator/internal/controller/datadogagent/defaults"
"github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature"
"github.com/DataDog/datadog-operator/pkg/agentprofile"
Expand Down Expand Up @@ -86,7 +87,13 @@ func (r *Reconciler) reconcileInstanceV3(ctx context.Context, logger logr.Logger
// TODO: introspection
sendProfileEnabledMetric(r.options.DatadogAgentProfileEnabled)
if r.options.DatadogAgentProfileEnabled {
appliedProfiles, e := r.reconcileProfiles(ctx)
dsName := component.GetDaemonSetNameFromDatadogAgent(instance, &instance.Spec)
dsNSName := types.NamespacedName{
Namespace: instance.Namespace,
Name: dsName,
}
maxUnavailable := agentprofile.GetMaxUnavailableFromSpecAndEDS(&instance.Spec, &r.options.ExtendedDaemonsetOptions, nil)
appliedProfiles, e := r.reconcileProfiles(ctx, dsNSName, maxUnavailable)
if e != nil {
return r.updateStatusIfNeededV2(logger, instance, ddaStatusCopy, result, e, now)
}
Expand Down
61 changes: 55 additions & 6 deletions internal/controller/datadogagent/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ import (
"fmt"

"github.com/prometheus/client_golang/prometheus"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"

v1alpha1 "github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1"
v2alpha1 "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1"
Expand All @@ -36,7 +39,7 @@ func sendProfileEnabledMetric(enabled bool) {
// - returns a list of profiles that should be applied (including the default profile)
// - configures node labels based on the profiles that are applied
// - applies profile status updates in k8s
func (r *Reconciler) reconcileProfiles(ctx context.Context) ([]*v1alpha1.DatadogAgentProfile, error) {
func (r *Reconciler) reconcileProfiles(ctx context.Context, dsNSName types.NamespacedName, ddaEDSMaxUnavailable intstr.IntOrString) ([]*v1alpha1.DatadogAgentProfile, error) {
now := metav1.Now()
// start with the default profile so that on error, at minimum the default profile is applied
defaultProfile := agentprofile.DefaultProfile()
Expand All @@ -53,11 +56,18 @@ func (r *Reconciler) reconcileProfiles(ctx context.Context) ([]*v1alpha1.Datadog
return appliedProfiles, fmt.Errorf("unable to get node list: %w", err)
}

// profilesByNode is a map of node name to profile
profilesByNode := make(map[string]types.NamespacedName)
// profilesByNode maps node name to profile
// it is pre-populated with the default profile
profilesByNode := make(map[string]types.NamespacedName, len(nodeList))
for _, node := range nodeList {
profilesByNode[node.Name] = types.NamespacedName{Namespace: "", Name: "default"}
}

// csInfo holds create strategy data per profile
csInfo := make(map[types.NamespacedName]*agentprofile.CreateStrategyInfo)
for _, profile := range sortedProfiles {
profileCopy := profile.DeepCopy() // deep copy to avoid modifying status of original profile
if err := r.reconcileProfile(ctx, profileCopy, nodeList, profilesByNode, now); err != nil {
if err := r.reconcileProfile(ctx, profileCopy, nodeList, profilesByNode, csInfo, now); err != nil {
// errors will be validation or conflict errors
r.log.Error(err, "unable to reconcile profile", "datadogagentprofile", profileCopy.Name, "datadogagentprofile_namespace", profileCopy.Namespace)
}
Expand All @@ -73,6 +83,29 @@ func (r *Reconciler) reconcileProfiles(ctx context.Context) ([]*v1alpha1.Datadog
}
}

// create strategy
if agentprofile.CreateStrategyEnabled() {
for _, profile := range appliedProfiles {
if !agentprofile.CreateStrategyNeeded(profile, csInfo) {
continue
}

// get ds to set create strategy status
ds, err := r.getProfileDaemonSet(ctx, profile, dsNSName)
if err != nil {
return appliedProfiles, fmt.Errorf("unable to get profile daemon set: %w", err)
}

profileCopy := profile.DeepCopy()
agentprofile.ApplyCreateStrategy(r.log, profilesByNode, csInfo[types.NamespacedName{Namespace: profile.Namespace, Name: profile.Name}], profileCopy, ddaEDSMaxUnavailable, len(nodeList), &ds.Status)
if !agentprofile.IsEqualStatus(&profile.Status, &profileCopy.Status) {
if err := r.client.Status().Update(ctx, profileCopy); err != nil {
r.log.Error(err, "unable to update profile status", "datadogagentprofile", profileCopy.Name, "datadogagentprofile_namespace", profileCopy.Namespace)
}
}
}
}

// label nodes
if err := r.labelNodesWithProfiles(ctx, profilesByNode); err != nil {
return appliedProfiles, fmt.Errorf("unable to label nodes with profiles: %w", err)
Expand All @@ -84,7 +117,7 @@ func (r *Reconciler) reconcileProfiles(ctx context.Context) ([]*v1alpha1.Datadog
// - validates the profile
// - checks for conflicts with existing profiles
// - updates the profile status based on profile validation and application success
func (r *Reconciler) reconcileProfile(ctx context.Context, profile *v1alpha1.DatadogAgentProfile, nodeList []corev1.Node, profilesByNode map[string]types.NamespacedName, now metav1.Time) error {
func (r *Reconciler) reconcileProfile(ctx context.Context, profile *v1alpha1.DatadogAgentProfile, nodeList []corev1.Node, profilesByNode map[string]types.NamespacedName, csInfo map[types.NamespacedName]*agentprofile.CreateStrategyInfo, now metav1.Time) error {
r.log.Info("reconciling profile", "datadogagentprofile", profile.Name, "datadogagentprofile_namespace", profile.Namespace)
// validate profile name, spec, and selectors
requirements, err := agentprofile.ValidateProfileAndReturnRequirements(profile, r.options.DatadogAgentInternalEnabled)
Expand All @@ -98,7 +131,7 @@ func (r *Reconciler) reconcileProfile(ctx context.Context, profile *v1alpha1.Dat
profile.Status.Conditions = agentprofile.SetDatadogAgentProfileCondition(profile.Status.Conditions, agentprofile.NewDatadogAgentProfileCondition(agentprofile.ValidConditionType, metav1.ConditionTrue, now, agentprofile.ValidConditionReason, "Valid manifest"))

// err can only be conflict
if err := agentprofile.ApplyProfileToNodes(profile.ObjectMeta, requirements, nodeList, profilesByNode); err != nil {
if err := agentprofile.ApplyProfileToNodes(profile.ObjectMeta, requirements, nodeList, profilesByNode, csInfo); err != nil {
profile.Status.Conditions = agentprofile.SetDatadogAgentProfileCondition(profile.Status.Conditions, agentprofile.NewDatadogAgentProfileCondition(agentprofile.AppliedConditionType, metav1.ConditionFalse, now, agentprofile.ConflictConditionReason, "Conflict with existing profile"))
return err
}
Expand All @@ -107,6 +140,22 @@ func (r *Reconciler) reconcileProfile(ctx context.Context, profile *v1alpha1.Dat
return nil
}

func (r *Reconciler) getProfileDaemonSet(ctx context.Context, profile *v1alpha1.DatadogAgentProfile, dsName types.NamespacedName) (*appsv1.DaemonSet, error) {
validDaemonSetNames, _ := r.getValidDaemonSetNames(dsName.Name, map[string]struct{}{}, []v1alpha1.DatadogAgentProfile{*profile}, true)
Copy link
Member

@tbavelier tbavelier Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
validDaemonSetNames, _ := r.getValidDaemonSetNames(dsName.Name, map[string]struct{}{}, []v1alpha1.DatadogAgentProfile{*profile}, true)
// TODO: introspection - we pass an empty provider list
validDaemonSetNames, _ := r.getValidDaemonSetNames(dsName.Name, map[string]struct{}{}, []v1alpha1.DatadogAgentProfile{*profile}, true)

Comment so when we start supporting introspection, so we don't forget ?

Copy link
Member

@tbavelier tbavelier Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: the spacing might be off, I manually added spaces when creating this suggestion, I wouldn't commit it from github UI

if len(validDaemonSetNames) != 1 {
return nil, fmt.Errorf("unexpected number of valid daemonset names: %d", len(validDaemonSetNames))
}

for name := range validDaemonSetNames {
ds := &appsv1.DaemonSet{}
if err := r.client.Get(ctx, types.NamespacedName{Namespace: dsName.Namespace, Name: name}, ds); err != nil && !apierrors.IsNotFound(err) {
return nil, err
}
return ds, nil
}
return nil, fmt.Errorf("no valid daemonset found")
}

func (r *Reconciler) applyProfilesToDDAISpec(ddai *v1alpha1.DatadogAgentInternal, profiles []*v1alpha1.DatadogAgentProfile) ([]*v1alpha1.DatadogAgentInternal, error) {
ddais := []*v1alpha1.DatadogAgentInternal{}

Expand Down
29 changes: 26 additions & 3 deletions internal/controller/datadogagent/profile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
Expand Down Expand Up @@ -822,6 +823,10 @@ func Test_reconcileProfile(t *testing.T) {
Name: "profile",
Namespace: "default",
},
"node2": {
Name: "default",
Namespace: "",
},
},
},
{
Expand Down Expand Up @@ -979,7 +984,12 @@ func Test_reconcileProfile(t *testing.T) {
},
},
},
wantProfilesByNode: make(map[string]types.NamespacedName),
wantProfilesByNode: map[string]types.NamespacedName{
"node1": {
Name: "default",
Namespace: "",
},
},
},
}

Expand All @@ -1000,8 +1010,16 @@ func Test_reconcileProfile(t *testing.T) {
},
}

// Pre-populate with default profile for nodes without a profile (matching production code)
for _, node := range tt.nodes {
if _, exists := tt.profilesByNode[node.Name]; !exists {
tt.profilesByNode[node.Name] = types.NamespacedName{Namespace: "", Name: "default"}
}
}

profileCopy := tt.profile.DeepCopy()
err := r.reconcileProfile(ctx, profileCopy, tt.nodes, tt.profilesByNode, now)
csInfo := make(map[types.NamespacedName]*agentprofile.CreateStrategyInfo)
err := r.reconcileProfile(ctx, profileCopy, tt.nodes, tt.profilesByNode, csInfo, now)

assert.Equal(t, tt.wantErr, err)
assert.Equal(t, tt.wantStatus, profileCopy.Status)
Expand Down Expand Up @@ -1190,7 +1208,12 @@ func Test_reconcileProfiles(t *testing.T) {
},
}

appliedProfiles, err := r.reconcileProfiles(ctx)
dsNSName := types.NamespacedName{
Namespace: "default",
Name: "datadog-agent",
}
maxUnavailable := intstr.FromInt(1)
appliedProfiles, err := r.reconcileProfiles(ctx, dsNSName, maxUnavailable)

assert.Equal(t, tt.wantErr, err)
assert.Equal(t, tt.wantAppliedProfiles, len(appliedProfiles))
Expand Down
Loading
Loading