diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index c685f51e5..52d755590 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -696,3 +696,29 @@ rules: - get - patch - update +- apiGroups: + - datadoghq.com + resources: + - datadogagentprofiles + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - datadoghq.com + resources: + - datadogagentprofiles/status + verbs: + - get + - patch + - update +- apiGroups: + - datadoghq.com + resources: + - datadogagentprofiles/finalizers + verbs: + - update diff --git a/controllers/datadogagent/controller_reconcile_agent.go b/controllers/datadogagent/controller_reconcile_agent.go index 6af95dddd..3745cea7c 100644 --- a/controllers/datadogagent/controller_reconcile_agent.go +++ b/controllers/datadogagent/controller_reconcile_agent.go @@ -7,26 +7,32 @@ package datadogagent import ( "context" + "fmt" "time" + "github.com/go-logr/logr" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/DataDog/datadog-operator/apis/datadoghq/common/v1" + "github.com/DataDog/datadog-operator/apis/datadoghq/v1alpha1" datadoghqv2alpha1 "github.com/DataDog/datadog-operator/apis/datadoghq/v2alpha1" apiutils "github.com/DataDog/datadog-operator/apis/utils" + ddacomponent "github.com/DataDog/datadog-operator/controllers/datadogagent/component" componentagent "github.com/DataDog/datadog-operator/controllers/datadogagent/component/agent" "github.com/DataDog/datadog-operator/controllers/datadogagent/feature" "github.com/DataDog/datadog-operator/controllers/datadogagent/override" + "github.com/DataDog/datadog-operator/pkg/agentprofile" "github.com/DataDog/datadog-operator/pkg/controller/utils/datadog" - + "github.com/DataDog/datadog-operator/pkg/kubernetes" edsv1alpha1 "github.com/DataDog/extendeddaemonset/api/v1alpha1" - "github.com/go-logr/logr" - appsv1 "k8s.io/api/apps/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/reconcile" ) -func (r *Reconciler) reconcileV2Agent(logger logr.Logger, requiredComponents feature.RequiredComponents, features []feature.Feature, dda *datadoghqv2alpha1.DatadogAgent, resourcesManager feature.ResourceManagers, newStatus *datadoghqv2alpha1.DatadogAgentStatus, requiredContainers []common.AgentContainerName) (reconcile.Result, error) { +func (r *Reconciler) reconcileV2Agent(logger logr.Logger, requiredComponents feature.RequiredComponents, features []feature.Feature, dda *datadoghqv2alpha1.DatadogAgent, resourcesManager feature.ResourceManagers, newStatus *datadoghqv2alpha1.DatadogAgentStatus, requiredContainers []common.AgentContainerName, profile *v1alpha1.DatadogAgentProfile) (reconcile.Result, error) { var result reconcile.Result var eds *edsv1alpha1.ExtendedDaemonSet var daemonset *appsv1.DaemonSet @@ -41,6 +47,8 @@ func (r *Reconciler) reconcileV2Agent(logger logr.Logger, requiredComponents fea agentEnabled := requiredComponents.Agent.IsEnabled() if r.options.ExtendedDaemonsetOptions.Enabled { + // TODO: handle profiles like we do for DaemonSets below + // Start by creating the Default Agent extendeddaemonset eds = componentagent.NewDefaultAgentExtendedDaemonset(dda, &r.options.ExtendedDaemonsetOptions, requiredContainers) podManagers = feature.NewPodTemplateManagers(&eds.Spec.Template) @@ -96,13 +104,23 @@ func (r *Reconciler) reconcileV2Agent(logger logr.Logger, requiredComponents fea } // If Override is defined for the node agent component, apply the override on the PodTemplateSpec, it will cascade to container. + var componentOverrides []*datadoghqv2alpha1.DatadogAgentComponentOverride if componentOverride, ok := dda.Spec.Override[datadoghqv2alpha1.NodeAgentComponentName]; ok { + componentOverrides = append(componentOverrides, componentOverride) + } + + // Apply overrides from profiles last, so they can override what's defined in the DDA. + overrideFromProfile := agentprofile.ComponentOverrideFromProfile(profile) + componentOverrides = append(componentOverrides, &overrideFromProfile) + + for _, componentOverride := range componentOverrides { if apiutils.BoolValue(componentOverride.Disabled) { disabledByOverride = true } override.PodTemplateSpec(logger, podManagers, componentOverride, datadoghqv2alpha1.NodeAgentComponentName, dda.Name) override.DaemonSet(daemonset, componentOverride) } + if disabledByOverride { if agentEnabled { // The override supersedes what's set in requiredComponents; update status to reflect the conflict @@ -182,3 +200,49 @@ func (r *Reconciler) cleanupV2ExtendedDaemonSet(logger logr.Logger, dda *datadog return reconcile.Result{}, nil } + +func (r *Reconciler) cleanupDaemonSetsForProfilesThatNoLongerApply(ctx context.Context, dda *datadoghqv2alpha1.DatadogAgent, daemonSetNamesAppliedProfiles map[string]struct{}) error { + daemonSets, err := r.agentDaemonSetsCreatedByOperator(ctx) + if err != nil { + return err + } + + defaultDaemonSetName := ddacomponent.GetAgentName(dda) // TODO: take into account name override + for _, daemonSet := range daemonSets { + _, belongsToActiveProfile := daemonSetNamesAppliedProfiles[daemonSet.Name] + + if belongsToActiveProfile || daemonSet.Name == defaultDaemonSetName { + continue + } + + if err = r.client.Delete(ctx, &daemonSet); err != nil { + return err + } + } + + return nil +} + +// TODO: add specific labels to the daemonset created by the operator that belong to a profile so that we can filter more easily +func (r *Reconciler) agentDaemonSetsCreatedByOperator(ctx context.Context) ([]appsv1.DaemonSet, error) { + daemonSetList := appsv1.DaemonSetList{} + + err := r.client.List( + ctx, + &daemonSetList, + client.HasLabels{ + fmt.Sprintf( + "%s=%s,%s=%s", + kubernetes.AppKubernetesNameLabelKey, + "datadog-agent-deployment", + kubernetes.AppKubernetesManageByLabelKey, + "datadog-operator", + ), + }, + ) + if err != nil { + return nil, err + } + + return daemonSetList.Items, nil +} diff --git a/controllers/datadogagent/controller_reconcile_v2.go b/controllers/datadogagent/controller_reconcile_v2.go index 45b30e907..d598aefab 100644 --- a/controllers/datadogagent/controller_reconcile_v2.go +++ b/controllers/datadogagent/controller_reconcile_v2.go @@ -15,15 +15,18 @@ import ( apiequality "k8s.io/apimachinery/pkg/api/equality" 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/errors" "sigs.k8s.io/controller-runtime/pkg/reconcile" apicommon "github.com/DataDog/datadog-operator/apis/datadoghq/common" commonv1 "github.com/DataDog/datadog-operator/apis/datadoghq/common/v1" + datadoghqv1alpha1 "github.com/DataDog/datadog-operator/apis/datadoghq/v1alpha1" datadoghqv2alpha1 "github.com/DataDog/datadog-operator/apis/datadoghq/v2alpha1" "github.com/DataDog/datadog-operator/controllers/datadogagent/dependencies" "github.com/DataDog/datadog-operator/controllers/datadogagent/feature" "github.com/DataDog/datadog-operator/controllers/datadogagent/override" + "github.com/DataDog/datadog-operator/pkg/agentprofile" "github.com/DataDog/datadog-operator/pkg/controller/utils" "github.com/DataDog/datadog-operator/pkg/kubernetes" ) @@ -141,9 +144,27 @@ func (r *Reconciler) reconcileInstanceV2(ctx context.Context, logger logr.Logger } requiredContainers := requiredComponents.Agent.Containers - result, err = r.reconcileV2Agent(logger, requiredComponents, features, instance, resourceManagers, newStatus, requiredContainers) - if utils.ShouldReturn(result, err) { - return r.updateStatusIfNeededV2(logger, instance, newStatus, result, err) + + profiles, err := r.profilesToApply(ctx) + if err != nil { + return reconcile.Result{}, err + } + + daemonSetNamesAppliedProfiles := map[string]struct{}{} // TODO: do the same for EDS + for _, profile := range profiles { + result, err = r.reconcileV2Agent(logger, requiredComponents, features, instance, resourceManagers, newStatus, requiredContainers, &profile) + if utils.ShouldReturn(result, err) { + return r.updateStatusIfNeededV2(logger, instance, newStatus, result, err) + } + profileNamespacedName := types.NamespacedName{ + Namespace: profile.Namespace, + Name: profile.Name, + } + daemonSetNamesAppliedProfiles[agentprofile.DaemonSetName(profileNamespacedName)] = struct{}{} + } + // TODO: do the same for EDS. Also make sure that this doesn't delete anything that it isn't supposed to when there's an error in the for above that doesn't return + if err = r.cleanupDaemonSetsForProfilesThatNoLongerApply(ctx, instance, daemonSetNamesAppliedProfiles); err != nil { + return reconcile.Result{}, err } result, err = r.reconcileV2ClusterChecksRunner(logger, requiredComponents, features, instance, resourceManagers, newStatus) @@ -252,3 +273,12 @@ func (r *Reconciler) updateMetricsForwardersFeatures(dda *datadoghqv2alpha1.Data // r.forwarders.SetEnabledFeatures(dda, features) // } } + +func (r *Reconciler) profilesToApply(ctx context.Context) ([]datadoghqv1alpha1.DatadogAgentProfile, error) { + profilesList := datadoghqv1alpha1.DatadogAgentProfileList{} + err := r.client.List(ctx, &profilesList) + if err != nil { + return nil, err + } + return agentprofile.ProfilesToApply(profilesList.Items), nil +} diff --git a/controllers/datadogagent/override/container.go b/controllers/datadogagent/override/container.go index 082e2f15d..7210de080 100644 --- a/controllers/datadogagent/override/container.go +++ b/controllers/datadogagent/override/container.go @@ -109,7 +109,27 @@ func overrideContainer(container *corev1.Container, override *v2alpha1.DatadogAg } if override.Resources != nil { - container.Resources = *override.Resources + for resource, quantity := range override.Resources.Requests { + if quantity.IsZero() { + continue + } + + if container.Resources.Requests == nil { + container.Resources.Requests = corev1.ResourceList{} + } + container.Resources.Requests[resource] = quantity + } + + for resource, quantity := range override.Resources.Limits { + if quantity.IsZero() { + continue + } + + if container.Resources.Limits == nil { + container.Resources.Limits = corev1.ResourceList{} + } + container.Resources.Limits[resource] = quantity + } } if override.Command != nil { diff --git a/controllers/datadogagent/override/container_test.go b/controllers/datadogagent/override/container_test.go index 068534137..dda73a856 100644 --- a/controllers/datadogagent/override/container_test.go +++ b/controllers/datadogagent/override/container_test.go @@ -7,11 +7,12 @@ package override import ( "fmt" + "reflect" + "testing" + "github.com/DataDog/datadog-operator/apis/datadoghq/common" apiutils "github.com/DataDog/datadog-operator/apis/utils" "k8s.io/apimachinery/pkg/api/resource" - "reflect" - "testing" commonv1 "github.com/DataDog/datadog-operator/apis/datadoghq/common/v1" "github.com/DataDog/datadog-operator/apis/datadoghq/v2alpha1" @@ -176,7 +177,7 @@ func TestContainer(t *testing.T) { }, }, { - name: "override resources", + name: "override resources - when there are none defined", containerName: commonv1.CoreAgentContainerName, existingManager: func() *fake.PodTemplateManagers { return fake.NewPodTemplateManagers(t, corev1.PodTemplateSpec{ @@ -210,6 +211,57 @@ func TestContainer(t *testing.T) { }) }, }, + { + name: "override resources - when there are some defined", + containerName: commonv1.CoreAgentContainerName, + existingManager: func() *fake.PodTemplateManagers { + return fake.NewPodTemplateManagers(t, corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: string(commonv1.CoreAgentContainerName), + Resources: corev1.ResourceRequirements{ + Limits: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI), // Not overridden, should be kept + corev1.ResourceMemory: *resource.NewQuantity(2048, resource.DecimalSI), + }, + Requests: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: *resource.NewQuantity(1, resource.DecimalSI), + corev1.ResourceMemory: *resource.NewQuantity(1024, resource.DecimalSI), // Not overridden, should be kept + }, + }, + }, + }, + }, + }) + }, + override: v2alpha1.DatadogAgentGenericContainer{ + Resources: &corev1.ResourceRequirements{ + Limits: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceMemory: *resource.NewQuantity(4096, resource.DecimalSI), + }, + Requests: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI), + }, + }, + }, + validateManager: func(t *testing.T, manager *fake.PodTemplateManagers, containerName string) { + assertContainerMatch(t, manager.PodTemplateSpec().Spec.Containers, containerName, func(container corev1.Container) bool { + return reflect.DeepEqual( + corev1.ResourceRequirements{ + Limits: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI), // Not overridden + corev1.ResourceMemory: *resource.NewQuantity(4096, resource.DecimalSI), // Overridden + }, + Requests: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: *resource.NewQuantity(2, resource.DecimalSI), // Overridden + corev1.ResourceMemory: *resource.NewQuantity(1024, resource.DecimalSI), // Not overridden + }, + }, + container.Resources) + }) + }, + }, { name: "override command", containerName: commonv1.CoreAgentContainerName, diff --git a/controllers/datadogagent/override/daemonset.go b/controllers/datadogagent/override/daemonset.go index 3958cf49b..ae37aaebf 100644 --- a/controllers/datadogagent/override/daemonset.go +++ b/controllers/datadogagent/override/daemonset.go @@ -13,14 +13,14 @@ import ( // DaemonSet overrides a DaemonSet according to the given override options func DaemonSet(daemonSet *v1.DaemonSet, override *v2alpha1.DatadogAgentComponentOverride) { - if override.Name != nil { + if override.Name != nil && *override.Name != "" { daemonSet.Name = *override.Name } } // ExtendedDaemonSet overrides an ExtendedDaemonSet according to the given override options func ExtendedDaemonSet(eds *edsv1alpha1.ExtendedDaemonSet, override *v2alpha1.DatadogAgentComponentOverride) { - if override.Name != nil { + if override.Name != nil && *override.Name != "" { eds.Name = *override.Name } } diff --git a/controllers/datadogagent_controller.go b/controllers/datadogagent_controller.go index 0a940e760..c3e0723ec 100644 --- a/controllers/datadogagent_controller.go +++ b/controllers/datadogagent_controller.go @@ -190,6 +190,8 @@ func (r *DatadogAgentReconciler) SetupWithManager(mgr ctrl.Manager) error { Owns(r.PlatformInfo.CreatePDBObject()). Owns(&networkingv1.NetworkPolicy{}) + builder.Watches(&source.Kind{Type: &datadoghqv1alpha1.DatadogAgentProfile{}}, &handler.EnqueueRequestForObject{}) + // DatadogAgent is namespaced whereas ClusterRole and ClusterRoleBinding are // cluster-scoped. That means that DatadogAgent cannot be their owner, and // we cannot use .Owns(). diff --git a/pkg/agentprofile/agent_profile.go b/pkg/agentprofile/agent_profile.go new file mode 100644 index 000000000..8885e810c --- /dev/null +++ b/pkg/agentprofile/agent_profile.go @@ -0,0 +1,173 @@ +// 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 agentprofile + +import ( + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/DataDog/datadog-operator/apis/datadoghq/common/v1" + datadoghqv1alpha1 "github.com/DataDog/datadog-operator/apis/datadoghq/v1alpha1" + "github.com/DataDog/datadog-operator/apis/datadoghq/v2alpha1" +) + +const ( + defaultProfileName = "default" + daemonSetNamePrefix = "datadog-agent-with-profile-" +) + +// ProfilesToApply given a list of profiles, returns the ones that should be +// applied in the cluster. +// - If there are no profiles, it returns the default profile. +// - If there are no conflicting profiles, it returns all the profiles plus the default one. +// - If there are conflicting profiles, it returns a subset that does not +// conflict plus the default one. When there are conflicting profiles, the +// oldest one is the one that takes precedence. +func ProfilesToApply(profiles []datadoghqv1alpha1.DatadogAgentProfile) []datadoghqv1alpha1.DatadogAgentProfile { + var res []datadoghqv1alpha1.DatadogAgentProfile + + // TODO: detect conflicts here and only add the ones that are not + // conflicting (Give precedence to the oldest profile). + // This function will need the list of hosts to check for conflicts. + res = append(res, profiles...) + + return append(res, defaultProfile(res)) +} + +// ComponentOverrideFromProfile returns the component override that should be +// applied according to the given profile. +func ComponentOverrideFromProfile(profile *datadoghqv1alpha1.DatadogAgentProfile) v2alpha1.DatadogAgentComponentOverride { + overrideDSName := DaemonSetName(types.NamespacedName{ + Namespace: profile.Namespace, + Name: profile.Name, + }) + + return v2alpha1.DatadogAgentComponentOverride{ + Name: &overrideDSName, + Affinity: affinityOverride(profile), + Containers: containersOverride(profile), + } +} + +// DaemonSetName returns the name that the DaemonSet should have according to +// the name of the profile associated with it. +func DaemonSetName(profileNamespacedName types.NamespacedName) string { + if profileNamespacedName.Name == defaultProfileName { + return "" // Return empty so it does not override the default DaemonSet name + } + + return daemonSetNamePrefix + profileNamespacedName.Namespace + "-" + profileNamespacedName.Name +} + +// defaultProfile returns the default profile, which is the one to be applied in +// the nodes where none of the profiles received apply. +// Note: this function assumes that the profiles received do not conflict. +func defaultProfile(profiles []datadoghqv1alpha1.DatadogAgentProfile) datadoghqv1alpha1.DatadogAgentProfile { + var nodeSelectorRequirements []v1.NodeSelectorRequirement + + // TODO: I think this strategy only works if there's only one node selector per profile. + for _, profile := range profiles { + if profile.Spec.ProfileAffinity != nil { + for _, nodeSelectorRequirement := range profile.Spec.ProfileAffinity.ProfileNodeAffinity { + nodeSelectorRequirements = append( + nodeSelectorRequirements, + v1.NodeSelectorRequirement{ + Key: nodeSelectorRequirement.Key, + Operator: oppositeOperator(nodeSelectorRequirement.Operator), + Values: nodeSelectorRequirement.Values, + }, + ) + } + } + } + + profile := datadoghqv1alpha1.DatadogAgentProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: defaultProfileName, + }, + } + + if len(nodeSelectorRequirements) > 0 { + profile.Spec.ProfileAffinity = &datadoghqv1alpha1.ProfileAffinity{ + ProfileNodeAffinity: nodeSelectorRequirements, + } + } + + return profile +} + +func oppositeOperator(op v1.NodeSelectorOperator) v1.NodeSelectorOperator { + switch op { + case v1.NodeSelectorOpIn: + return v1.NodeSelectorOpNotIn + case v1.NodeSelectorOpNotIn: + return v1.NodeSelectorOpIn + case v1.NodeSelectorOpExists: + return v1.NodeSelectorOpDoesNotExist + case v1.NodeSelectorOpDoesNotExist: + return v1.NodeSelectorOpExists + case v1.NodeSelectorOpGt: + return v1.NodeSelectorOpLt + case v1.NodeSelectorOpLt: + return v1.NodeSelectorOpGt + default: + return "" + } +} + +func affinityOverride(profile *datadoghqv1alpha1.DatadogAgentProfile) *v1.Affinity { + if profile.Spec.ProfileAffinity == nil || len(profile.Spec.ProfileAffinity.ProfileNodeAffinity) == 0 { + return nil + } + + return &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: profile.Spec.ProfileAffinity.ProfileNodeAffinity, + }, + }, + }, + }, + } +} + +func containersOverride(profile *datadoghqv1alpha1.DatadogAgentProfile) map[common.AgentContainerName]*v2alpha1.DatadogAgentGenericContainer { + if profile.Spec.Config == nil { + return nil + } + + nodeAgentOverride, ok := profile.Spec.Config.Override[datadoghqv1alpha1.NodeAgentComponentName] + if !ok { // We only support overrides for the node agent, if there is no override for it, there's nothing to do + return nil + } + + if len(nodeAgentOverride.Containers) == 0 { + return nil + } + + containersInNodeAgent := []common.AgentContainerName{ + common.CoreAgentContainerName, + common.TraceAgentContainerName, + common.ProcessAgentContainerName, + common.SecurityAgentContainerName, + common.SystemProbeContainerName, + } + + res := map[common.AgentContainerName]*v2alpha1.DatadogAgentGenericContainer{} + + for _, containerName := range containersInNodeAgent { + if overrideForContainer, overrideIsDefined := nodeAgentOverride.Containers[containerName]; overrideIsDefined { + res[containerName] = &v2alpha1.DatadogAgentGenericContainer{ + Resources: overrideForContainer.Resources, + } + } + } + + return res +} diff --git a/pkg/agentprofile/agent_profile_test.go b/pkg/agentprofile/agent_profile_test.go new file mode 100644 index 000000000..e0a7da21b --- /dev/null +++ b/pkg/agentprofile/agent_profile_test.go @@ -0,0 +1,246 @@ +// 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 agentprofile + +import ( + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/DataDog/datadog-operator/apis/datadoghq/common/v1" + "github.com/DataDog/datadog-operator/apis/datadoghq/v1alpha1" + "github.com/DataDog/datadog-operator/apis/datadoghq/v2alpha1" +) + +func TestProfilesToApply(t *testing.T) { + tests := []struct { + name string + profiles []v1alpha1.DatadogAgentProfile + expectedProfiles []v1alpha1.DatadogAgentProfile + }{ + { + name: "no profiles", + profiles: []v1alpha1.DatadogAgentProfile{}, + expectedProfiles: []v1alpha1.DatadogAgentProfile{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + }, + Spec: v1alpha1.DatadogAgentProfileSpec{ + ProfileAffinity: nil, // Applies to all nodes + Config: nil, // No overrides + }, + }, + }, + }, + { + name: "several non-conflicting profiles", + profiles: []v1alpha1.DatadogAgentProfile{ + exampleProfileForLinux(), + exampleProfileForWindows(), + }, + expectedProfiles: []v1alpha1.DatadogAgentProfile{ + exampleProfileForLinux(), + exampleProfileForWindows(), + { // Default that does not apply to linux or windows + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + }, + Spec: v1alpha1.DatadogAgentProfileSpec{ + ProfileAffinity: &v1alpha1.ProfileAffinity{ + ProfileNodeAffinity: []v1.NodeSelectorRequirement{ + { // Opposite of example Linux profile + Key: "os", + Operator: v1.NodeSelectorOpNotIn, + Values: []string{"linux"}, + }, + { // Opposite of example Windows profile + Key: "os", + Operator: v1.NodeSelectorOpNotIn, + Values: []string{"windows"}, + }, + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.ElementsMatch(t, test.expectedProfiles, ProfilesToApply(test.profiles)) + }) + } +} + +func TestComponentOverrideFromProfile(t *testing.T) { + overrideNameForLinuxProfile := "datadog-agent-with-profile-default-linux" + overrideNameForExampleProfile := "datadog-agent-with-profile-default-example" + + tests := []struct { + name string + profile v1alpha1.DatadogAgentProfile + expectedOverride v2alpha1.DatadogAgentComponentOverride + }{ + { + name: "profile without affinity or config", + profile: v1alpha1.DatadogAgentProfile{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "example", + }, + }, + expectedOverride: v2alpha1.DatadogAgentComponentOverride{ + Name: &overrideNameForExampleProfile, + }, + }, + { + name: "profile with affinity and config", + profile: exampleProfileForLinux(), + expectedOverride: v2alpha1.DatadogAgentComponentOverride{ + Name: &overrideNameForLinuxProfile, + Affinity: &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "os", + Operator: v1.NodeSelectorOpIn, + Values: []string{"linux"}, + }, + }, + }, + }, + }, + }, + }, + Containers: map[common.AgentContainerName]*v2alpha1.DatadogAgentGenericContainer{ + common.CoreAgentContainerName: { + Resources: &v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("100m"), + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expectedOverride, ComponentOverrideFromProfile(&test.profile)) + }) + } +} + +func TestDaemonSetName(t *testing.T) { + tests := []struct { + name string + profileNamespacedName types.NamespacedName + expectedDaemonSetName string + }{ + { + name: "default profile name", + profileNamespacedName: types.NamespacedName{ + Namespace: "agent", + Name: "default", + }, + expectedDaemonSetName: "", + }, + { + name: "non-default profile name", + profileNamespacedName: types.NamespacedName{ + Namespace: "agent", + Name: "linux", + }, + expectedDaemonSetName: "datadog-agent-with-profile-agent-linux", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expectedDaemonSetName, DaemonSetName(test.profileNamespacedName)) + }) + } +} + +func exampleProfileForLinux() v1alpha1.DatadogAgentProfile { + return v1alpha1.DatadogAgentProfile{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "linux", + }, + Spec: v1alpha1.DatadogAgentProfileSpec{ + ProfileAffinity: &v1alpha1.ProfileAffinity{ + ProfileNodeAffinity: []v1.NodeSelectorRequirement{ + { + Key: "os", + Operator: v1.NodeSelectorOpIn, + Values: []string{"linux"}, + }, + }, + }, + Config: &v1alpha1.Config{ + Override: map[v1alpha1.ComponentName]*v1alpha1.Override{ + v1alpha1.NodeAgentComponentName: { + Containers: map[common.AgentContainerName]*v1alpha1.Container{ + common.CoreAgentContainerName: { + Resources: &v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("100m"), + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func exampleProfileForWindows() v1alpha1.DatadogAgentProfile { + return v1alpha1.DatadogAgentProfile{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "windows", + }, + Spec: v1alpha1.DatadogAgentProfileSpec{ + ProfileAffinity: &v1alpha1.ProfileAffinity{ + ProfileNodeAffinity: []v1.NodeSelectorRequirement{ + { + Key: "os", + Operator: v1.NodeSelectorOpIn, + Values: []string{"windows"}, + }, + }, + }, + Config: &v1alpha1.Config{ + Override: map[v1alpha1.ComponentName]*v1alpha1.Override{ + v1alpha1.NodeAgentComponentName: { + Containers: map[common.AgentContainerName]*v1alpha1.Container{ + common.CoreAgentContainerName: { + Resources: &v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("200m"), + }, + }, + }, + }, + }, + }, + }, + }, + } +}