From ca228e4c57a7d7a6db901dd3380248236b99bbef Mon Sep 17 00:00:00 2001 From: khewonc <39867936+khewonc@users.noreply.github.com> Date: Tue, 13 Jan 2026 16:22:57 -0500 Subject: [PATCH] Add configmap preprocessor --- .../datadogagent/component_clusteragent.go | 7 ++ .../component_clusterchecksrunner.go | 6 ++ .../controller_reconcile_agent.go | 12 ++++ .../feature/kubernetesstatecore/configmap.go | 18 ++++- .../kubernetesstatecore/configmap_test.go | 46 ++++++++++--- .../feature/kubernetesstatecore/feature.go | 31 --------- .../kubernetesstatecore/feature_test.go | 33 +-------- .../datadogagent/store/preprocess.go | 69 +++++++++++++++++-- .../controller/datadogagent/store/store.go | 25 +++++-- .../component_clusteragent.go | 6 ++ .../component_clusterchecksrunner.go | 6 ++ .../controller_reconcile_agent.go | 12 ++++ pkg/constants/const.go | 4 ++ pkg/constants/utils.go | 4 ++ pkg/kubernetes/const.go | 3 + 15 files changed, 196 insertions(+), 86 deletions(-) diff --git a/internal/controller/datadogagent/component_clusteragent.go b/internal/controller/datadogagent/component_clusteragent.go index 0b0537dab..f9ca01fc6 100644 --- a/internal/controller/datadogagent/component_clusteragent.go +++ b/internal/controller/datadogagent/component_clusteragent.go @@ -104,6 +104,13 @@ func (c *ClusterAgentComponent) Reconcile(ctx context.Context, params *Reconcile deployment.Labels[constants.MD5AgentDeploymentProviderLabelKey] = params.Provider } + // Apply MD5 hashes for ConfigMaps + cmAnnotations := params.ResourceManagers.Store().GetComponentAnnotations(c.Name()) + for key, value := range cmAnnotations { + podManagers.Annotation().AddAnnotation(key, value) + + } + return c.reconciler.createOrUpdateDeployment(params.Logger, params.DDA, deployment, params.Status, updateStatusV2WithClusterAgent) } diff --git a/internal/controller/datadogagent/component_clusterchecksrunner.go b/internal/controller/datadogagent/component_clusterchecksrunner.go index b803d2a5c..3731ebf5d 100644 --- a/internal/controller/datadogagent/component_clusterchecksrunner.go +++ b/internal/controller/datadogagent/component_clusterchecksrunner.go @@ -99,6 +99,12 @@ func (c *ClusterChecksRunnerComponent) Reconcile(ctx context.Context, params *Re override.Deployment(deployment, componentOverride) } + // Apply MD5 hashes for ConfigMaps + cmAnnotations := params.ResourceManagers.Store().GetComponentAnnotations(c.Name()) + for key, value := range cmAnnotations { + podManagers.Annotation().AddAnnotation(key, value) + } + return c.reconciler.createOrUpdateDeployment(params.Logger, params.DDA, deployment, params.Status, updateStatusV2WithClusterChecksRunner) } diff --git a/internal/controller/datadogagent/controller_reconcile_agent.go b/internal/controller/datadogagent/controller_reconcile_agent.go index d0154575f..00f353eb3 100644 --- a/internal/controller/datadogagent/controller_reconcile_agent.go +++ b/internal/controller/datadogagent/controller_reconcile_agent.go @@ -135,6 +135,12 @@ func (r *Reconciler) reconcileV2Agent(logger logr.Logger, requiredComponents fea return reconcile.Result{}, nil } + // Apply MD5 hashes for ConfigMaps + cmAnnotations := resourcesManager.Store().GetComponentAnnotations(datadoghqv2alpha1.NodeAgentComponentName) + for key, value := range cmAnnotations { + podManagers.Annotation().AddAnnotation(key, value) + } + return r.createOrUpdateExtendedDaemonset(daemonsetLogger, dda, eds, newStatus, updateEDSStatusV2WithAgent) } @@ -236,6 +242,12 @@ func (r *Reconciler) reconcileV2Agent(logger logr.Logger, requiredComponents fea return reconcile.Result{}, nil } + // Apply MD5 hashes for ConfigMaps + cmAnnotations := resourcesManager.Store().GetComponentAnnotations(datadoghqv2alpha1.NodeAgentComponentName) + for key, value := range cmAnnotations { + podManagers.Annotation().AddAnnotation(key, value) + } + return r.createOrUpdateDaemonset(daemonsetLogger, dda, daemonset, newStatus, updateDSStatusV2WithAgent, profile) } diff --git a/internal/controller/datadogagent/feature/kubernetesstatecore/configmap.go b/internal/controller/datadogagent/feature/kubernetesstatecore/configmap.go index 13d9e5b2a..73031e362 100644 --- a/internal/controller/datadogagent/feature/kubernetesstatecore/configmap.go +++ b/internal/controller/datadogagent/feature/kubernetesstatecore/configmap.go @@ -13,18 +13,32 @@ import ( 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/internal/controller/datadogagent/object/configmap" + "github.com/DataDog/datadog-operator/pkg/constants" ) func (f *ksmFeature) buildKSMCoreConfigMap(collectorOpts collectorOptions) (*corev1.ConfigMap, error) { if f.customConfig != nil && f.customConfig.ConfigMap != nil { return nil, nil } + + var configMap *corev1.ConfigMap if f.customConfig != nil && f.customConfig.ConfigData != nil { - return configmap.BuildConfigMapConfigData(f.owner.GetNamespace(), f.customConfig.ConfigData, f.configConfigMapName, ksmCoreCheckName) + var err error + configMap, err = configmap.BuildConfigMapConfigData(f.owner.GetNamespace(), f.customConfig.ConfigData, f.configConfigMapName, ksmCoreCheckName) + if err != nil { + return nil, err + } + } else { + configMap = buildDefaultConfigMap(f.owner.GetNamespace(), f.configConfigMapName, ksmCheckConfig(f.runInClusterChecksRunner, collectorOpts)) + } + + configMap.Labels = map[string]string{ + constants.ConfigIDLabelKey: string(f.ID()), + constants.GetOperatorComponentLabelKey(v2alpha1.ClusterAgentComponentName): "true", } - configMap := buildDefaultConfigMap(f.owner.GetNamespace(), f.configConfigMapName, ksmCheckConfig(f.runInClusterChecksRunner, collectorOpts)) return configMap, nil } diff --git a/internal/controller/datadogagent/feature/kubernetesstatecore/configmap_test.go b/internal/controller/datadogagent/feature/kubernetesstatecore/configmap_test.go index 244167aab..9effa9995 100644 --- a/internal/controller/datadogagent/feature/kubernetesstatecore/configmap_test.go +++ b/internal/controller/datadogagent/feature/kubernetesstatecore/configmap_test.go @@ -10,11 +10,35 @@ import ( "testing" "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" + "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature" + "github.com/DataDog/datadog-operator/pkg/constants" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// buildExpectedConfigMap creates the expected ConfigMap for testing +func buildExpectedConfigMap(namespace, name string, opts collectorOptions, runInClusterChecksRunner bool, customContent string) *corev1.ConfigMap { + content := customContent + if content == "" { + content = ksmCheckConfig(runInClusterChecksRunner, opts) + } + + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: map[string]string{ + constants.ConfigIDLabelKey: string(feature.KubernetesStateCoreIDType), + constants.GetOperatorComponentLabelKey(v2alpha1.ClusterAgentComponentName): "true", + }, + }, + Data: map[string]string{ + ksmCoreCheckName: content, + }, + } +} + func Test_ksmFeature_buildKSMCoreConfigMap(t *testing.T) { owner := &metav1.ObjectMeta{ Name: "test", @@ -102,7 +126,7 @@ instances: runInClusterChecksRunner: true, configConfigMapName: defaultKubeStateMetricsCoreConf, }, - want: buildDefaultConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, ksmCheckConfig(true, defaultOptions)), + want: buildExpectedConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, defaultOptions, true, ""), }, { name: "override", @@ -115,7 +139,7 @@ instances: ConfigData: &overrideConf, }, }, - want: buildDefaultConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, overrideConf), + want: buildExpectedConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, collectorOptions{}, true, overrideConf), }, { name: "no cluster check runners", @@ -125,7 +149,7 @@ instances: runInClusterChecksRunner: false, configConfigMapName: defaultKubeStateMetricsCoreConf, }, - want: buildDefaultConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, ksmCheckConfig(false, defaultOptions)), + want: buildExpectedConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, defaultOptions, false, ""), }, { name: "with vpa", @@ -136,7 +160,7 @@ instances: configConfigMapName: defaultKubeStateMetricsCoreConf, collectorOpts: optionsWithVPA, }, - want: buildDefaultConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, ksmCheckConfig(true, optionsWithVPA)), + want: buildExpectedConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, optionsWithVPA, true, ""), }, { name: "with CRDs", @@ -147,7 +171,7 @@ instances: configConfigMapName: defaultKubeStateMetricsCoreConf, collectorOpts: optionsWithCRD, }, - want: buildDefaultConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, ksmCheckConfig(true, optionsWithCRD)), + want: buildExpectedConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, optionsWithCRD, true, ""), }, { name: "with APIServices", @@ -158,7 +182,7 @@ instances: configConfigMapName: defaultKubeStateMetricsCoreConf, collectorOpts: optionsWithAPIService, }, - want: buildDefaultConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, ksmCheckConfig(true, optionsWithAPIService)), + want: buildExpectedConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, optionsWithAPIService, true, ""), }, { name: "with ControllerRevisions", @@ -169,7 +193,7 @@ instances: configConfigMapName: defaultKubeStateMetricsCoreConf, collectorOpts: optionsWithControllerRevisions, }, - want: buildDefaultConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, ksmCheckConfig(true, optionsWithControllerRevisions)), + want: buildExpectedConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, optionsWithControllerRevisions, true, ""), }, { name: "with custom resources", @@ -180,7 +204,7 @@ instances: configConfigMapName: defaultKubeStateMetricsCoreConf, collectorOpts: optionsWithCustomResources, }, - want: buildDefaultConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, ksmCheckConfig(true, optionsWithCustomResources)), + want: buildExpectedConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, optionsWithCustomResources, true, ""), }, { name: "with multiple custom resources", @@ -191,7 +215,7 @@ instances: configConfigMapName: defaultKubeStateMetricsCoreConf, collectorOpts: optionsWithMultipleCustomResources, }, - want: buildDefaultConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, ksmCheckConfig(true, optionsWithMultipleCustomResources)), + want: buildExpectedConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, optionsWithMultipleCustomResources, true, ""), }, { name: "with VPA and custom resources", @@ -202,7 +226,7 @@ instances: configConfigMapName: defaultKubeStateMetricsCoreConf, collectorOpts: optionsWithVPAAndCustomResources, }, - want: buildDefaultConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, ksmCheckConfig(true, optionsWithVPAAndCustomResources)), + want: buildExpectedConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, optionsWithVPAAndCustomResources, true, ""), }, { name: "with custom resources and no cluster check", @@ -213,7 +237,7 @@ instances: configConfigMapName: defaultKubeStateMetricsCoreConf, collectorOpts: optionsWithCustomResources, }, - want: buildDefaultConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, ksmCheckConfig(false, optionsWithCustomResources)), + want: buildExpectedConfigMap(owner.GetNamespace(), defaultKubeStateMetricsCoreConf, optionsWithCustomResources, false, ""), }, } for _, tt := range tests { diff --git a/internal/controller/datadogagent/feature/kubernetesstatecore/feature.go b/internal/controller/datadogagent/feature/kubernetesstatecore/feature.go index dcf63dc23..3773a1949 100644 --- a/internal/controller/datadogagent/feature/kubernetesstatecore/feature.go +++ b/internal/controller/datadogagent/feature/kubernetesstatecore/feature.go @@ -18,10 +18,8 @@ import ( "github.com/DataDog/datadog-operator/internal/controller/datadogagent/common" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/merger" - "github.com/DataDog/datadog-operator/internal/controller/datadogagent/object" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/object/volume" "github.com/DataDog/datadog-operator/pkg/constants" - "github.com/DataDog/datadog-operator/pkg/controller/utils/comparison" "github.com/DataDog/datadog-operator/pkg/kubernetes" "github.com/DataDog/datadog-operator/pkg/utils" ) @@ -145,30 +143,6 @@ func (f *ksmFeature) Configure(dda metav1.Object, ddaSpec *v2alpha1.DatadogAgent if ddaSpec.Features.KubeStateMetricsCore.Conf != nil { f.customConfig = ddaSpec.Features.KubeStateMetricsCore.Conf - hash, err := comparison.GenerateMD5ForSpec(f.customConfig) - if err != nil { - f.logger.Error(err, "couldn't generate hash for ksm core custom config") - } else { - f.logger.V(2).Info("built ksm core from custom config", "hash", hash) - } - f.customConfigAnnotationValue = hash - f.customConfigAnnotationKey = object.GetChecksumAnnotationKey(feature.KubernetesStateCoreIDType) - } else { - // Generate dynamic checksum for default configuration (based on user provided collectCrMetrics field and whether or not APIServices/CRD metrics are collected) - defaultConfigData := map[string]any{ - "collect_crds": f.collectCRDMetrics, - "collect_apiservices": f.collectAPIServiceMetrics, - "collect_cr_metrics": f.collectCrMetrics, - } - - hash, err := comparison.GenerateMD5ForSpec(defaultConfigData) - if err != nil { - f.logger.Error(err, "couldn't generate hash for default ksm core config") - } else { - f.logger.V(2).Info("generated default ksm core config hash", "hash", hash, "config", defaultConfigData) - } - f.customConfigAnnotationValue = hash - f.customConfigAnnotationKey = object.GetChecksumAnnotationKey(feature.KubernetesStateCoreIDType) } f.configConfigMapName = constants.GetConfName(dda, f.customConfig, defaultKubeStateMetricsCoreConf) @@ -203,11 +177,6 @@ func (f *ksmFeature) ManageDependencies(managers feature.ResourceManagers, provi return err } if configCM != nil { - // Add md5 hash annotation for custom config - if f.customConfigAnnotationKey != "" && f.customConfigAnnotationValue != "" { - annotations := object.MergeAnnotationsLabels(f.logger, configCM.GetAnnotations(), map[string]string{f.customConfigAnnotationKey: f.customConfigAnnotationValue}, "*") - configCM.SetAnnotations(annotations) - } if err := managers.Store().AddOrUpdate(kubernetes.ConfigMapKind, configCM); err != nil { return err } diff --git a/internal/controller/datadogagent/feature/kubernetesstatecore/feature_test.go b/internal/controller/datadogagent/feature/kubernetesstatecore/feature_test.go index f07fb5cbd..85d955781 100644 --- a/internal/controller/datadogagent/feature/kubernetesstatecore/feature_test.go +++ b/internal/controller/datadogagent/feature/kubernetesstatecore/feature_test.go @@ -6,18 +6,14 @@ package kubernetesstatecore import ( - "fmt" "testing" apicommon "github.com/DataDog/datadog-operator/api/datadoghq/common" - "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" apiutils "github.com/DataDog/datadog-operator/api/utils" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/fake" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/test" mergerfake "github.com/DataDog/datadog-operator/internal/controller/datadogagent/merger/fake" - "github.com/DataDog/datadog-operator/pkg/constants" - "github.com/DataDog/datadog-operator/pkg/controller/utils/comparison" "github.com/DataDog/datadog-operator/pkg/testutils" "github.com/google/go-cmp/cmp" @@ -139,7 +135,7 @@ func Test_ksmFeature_Configure(t *testing.T) { tests.Run(t, buildKSMFeature) } -func ksmClusterAgentWantFunc(hasCustomConfig bool) *test.ComponentTest { +func ksmClusterAgentWantFunc(useClusterChecksRunner bool) *test.ComponentTest { return test.NewDefaultComponentTest().WithWantFunc( func(t testing.TB, mgrInterface feature.PodTemplateManagers) { mgr := mgrInterface.(*fake.PodTemplateManagers) @@ -156,33 +152,6 @@ func ksmClusterAgentWantFunc(hasCustomConfig bool) *test.ComponentTest { }, } assert.True(t, apiutils.IsEqualStruct(dcaEnvVars, want), "DCA envvars \ndiff = %s", cmp.Diff(dcaEnvVars, want)) - - if hasCustomConfig { - customConfig := v2alpha1.CustomConfig{ - ConfigData: apiutils.NewStringPointer(customData), - } - hash, err := comparison.GenerateMD5ForSpec(&customConfig) - assert.NoError(t, err) - wantAnnotations := map[string]string{ - fmt.Sprintf(constants.MD5ChecksumAnnotationKey, feature.KubernetesStateCoreIDType): hash, - } - annotations := mgr.AnnotationMgr.Annotations - assert.True(t, apiutils.IsEqualStruct(annotations, wantAnnotations), "Annotations \ndiff = %s", cmp.Diff(annotations, wantAnnotations)) - } else { - // Verify default config annotation - CRDs and APIServices collected, no custom resource metrics - defaultConfigData := map[string]any{ - "collect_crds": true, - "collect_apiservices": true, - "collect_cr_metrics": nil, - } - hash, err := comparison.GenerateMD5ForSpec(defaultConfigData) - assert.NoError(t, err) - wantAnnotations := map[string]string{ - fmt.Sprintf(constants.MD5ChecksumAnnotationKey, feature.KubernetesStateCoreIDType): hash, - } - annotations := mgr.AnnotationMgr.Annotations - assert.True(t, apiutils.IsEqualStruct(annotations, wantAnnotations), "Default config annotations \ndiff = %s", cmp.Diff(annotations, wantAnnotations)) - } }, ) } diff --git a/internal/controller/datadogagent/store/preprocess.go b/internal/controller/datadogagent/store/preprocess.go index cfa94c3dd..5dd276b19 100644 --- a/internal/controller/datadogagent/store/preprocess.go +++ b/internal/controller/datadogagent/store/preprocess.go @@ -7,11 +7,16 @@ package store import ( "fmt" + "strings" v1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" + "github.com/DataDog/datadog-operator/internal/controller/datadogagent/object" + "github.com/DataDog/datadog-operator/pkg/constants" + "github.com/DataDog/datadog-operator/pkg/controller/utils/comparison" "github.com/DataDog/datadog-operator/pkg/kubernetes" "github.com/DataDog/datadog-operator/pkg/kubernetes/rbac" ) @@ -19,12 +24,13 @@ import ( // preprocessorFunc defines a function type for preprocessing objects before apply. // objStore is the object from the store and objAPIServer is the object from the API server (if it exists). // objAPIServer will be nil for creates, non-nil for updates. -type preprocessorFunc func(objStore, objAPIServer client.Object) (client.Object, error) +type preprocessorFunc func(ds *Store, objStore, objAPIServer client.Object) (client.Object, error) var preprocessorRegistry = map[kubernetes.ObjectKind]preprocessorFunc{ kubernetes.ClusterRolesKind: preprocessClusterRole, kubernetes.RolesKind: preprocessRole, kubernetes.ServicesKind: preprocessService, + kubernetes.ConfigMapsKind: preprocessConfigMap, kubernetes.APIServiceKind: preprocessResourceVersion, kubernetes.CiliumNetworkPoliciesKind: preprocessResourceVersion, kubernetes.PodDisruptionBudgetsKind: preprocessResourceVersion, @@ -33,7 +39,7 @@ var preprocessorRegistry = map[kubernetes.ObjectKind]preprocessorFunc{ // applyPreprocessing applies registered preprocessor for the given kind, if any func (ds *Store) applyPreprocessing(kind kubernetes.ObjectKind, objStore, objAPIServer client.Object) (client.Object, error) { if preprocessor, exists := preprocessorRegistry[kind]; exists { - return preprocessor(objStore, objAPIServer) + return preprocessor(ds, objStore, objAPIServer) } return objStore, nil } @@ -41,7 +47,7 @@ func (ds *Store) applyPreprocessing(kind kubernetes.ObjectKind, objStore, objAPI // preprocessClusterRole holds preprocessing rules for ClusterRole // - normalizes policy rules to minimize duplicates // - ensures deterministic output -func preprocessClusterRole(objStore, objAPIServer client.Object) (client.Object, error) { +func preprocessClusterRole(ds *Store, objStore, objAPIServer client.Object) (client.Object, error) { cr, ok := objStore.(*rbacv1.ClusterRole) if !ok { return nil, fmt.Errorf("expected *rbacv1.ClusterRole, got %T", objStore) @@ -55,7 +61,7 @@ func preprocessClusterRole(objStore, objAPIServer client.Object) (client.Object, // preprocessRole holds preprocessing rules for Role // - normalizes policy rules to minimize duplicates // - ensures deterministic output -func preprocessRole(objStore, objAPIServer client.Object) (client.Object, error) { +func preprocessRole(ds *Store, objStore, objAPIServer client.Object) (client.Object, error) { role, ok := objStore.(*rbacv1.Role) if !ok { return nil, fmt.Errorf("expected *rbacv1.Role, got %T", objStore) @@ -69,7 +75,7 @@ func preprocessRole(objStore, objAPIServer client.Object) (client.Object, error) // preprocessService holds preprocessing rules for Service // - ClusterIP and ClusterIPs are immutable and must be preserved during updates // - sets the resource version from the API server object if it exists to prevent invalid value error -func preprocessService(objStore, objAPIServer client.Object) (client.Object, error) { +func preprocessService(ds *Store, objStore, objAPIServer client.Object) (client.Object, error) { svcStore, ok := objStore.(*v1.Service) if !ok { return nil, fmt.Errorf("expected *v1.Service, got %T", objStore) @@ -86,11 +92,62 @@ func preprocessService(objStore, objAPIServer client.Object) (client.Object, err return svcStore, nil } +// preprocessConfigMap holds preprocessing rules for ConfigMap +func preprocessConfigMap(ds *Store, objStore, objAPIServer client.Object) (client.Object, error) { + cm, ok := objStore.(*v1.ConfigMap) + if !ok { + return nil, fmt.Errorf("expected *v1.ConfigMap, got %T", objStore) + } + + // Generate annotation key from config ID + id, ok := cm.Labels[constants.ConfigIDLabelKey] + if !ok { + // For now, return the original configmap to continue to equality check + // TODO: replace once all configmaps have config ID and move to preprocess hash + // return nil, errors.New("config ID label not found for configmap, unable to store hash") + ds.logger.V(2).Info("config ID label not found for configmap, unable to store hash", "configmap", cm.Name) + return cm, nil + } + annotationKey := object.GetChecksumAnnotationKey(id) + + // Compute MD5 hash + hash, err := comparison.GenerateMD5ForSpec(cm.Data) + if err != nil { + return nil, err + } + + // Store hash for each component the configmap belongs to + components := GetComponentsFromLabels(cm.Labels) + for _, component := range components { + if ds.componentAnnotations[component] == nil { + ds.componentAnnotations[component] = make(map[string]string) + } + ds.componentAnnotations[component][annotationKey] = hash + } + + // Add hash annotation to configmap + cm.SetAnnotations(object.MergeAnnotationsLabels(ds.logger, cm.GetAnnotations(), map[string]string{annotationKey: hash}, "*")) + + return cm, nil +} + // preprocessResourceVersion sets the resource version from the API server object if it exists // Required for APIService, CiliumNetworkPolicies, and PodDisruptionBudgets -func preprocessResourceVersion(objStore, objAPIServer client.Object) (client.Object, error) { +func preprocessResourceVersion(ds *Store, objStore, objAPIServer client.Object) (client.Object, error) { if objAPIServer != nil { objStore.SetResourceVersion(objAPIServer.GetResourceVersion()) } return objStore, nil } + +// GetComponentsFromLabels extracts component names from labels +func GetComponentsFromLabels(labels map[string]string) []v2alpha1.ComponentName { + var components []v2alpha1.ComponentName + for key := range labels { + if strings.HasPrefix(key, constants.OperatorComponentLabelKeyPrefix) { + componentName := strings.TrimPrefix(key, constants.OperatorComponentLabelKeyPrefix) + components = append(components, v2alpha1.ComponentName(componentName)) + } + } + return components +} diff --git a/internal/controller/datadogagent/store/store.go b/internal/controller/datadogagent/store/store.go index 2f0589a5b..33046b354 100644 --- a/internal/controller/datadogagent/store/store.go +++ b/internal/controller/datadogagent/store/store.go @@ -22,6 +22,7 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + "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/object" "github.com/DataDog/datadog-operator/pkg/equality" @@ -46,13 +47,15 @@ type StoreClient interface { Delete(kind kubernetes.ObjectKind, namespace string, name string) bool DeleteAll(ctx context.Context, k8sClient client.Client) []error Logger() logr.Logger + GetComponentAnnotations(componentName v2alpha1.ComponentName) map[string]string } // NewStore returns a new Store instance func NewStore(owner metav1.Object, options *StoreOptions) *Store { store := &Store{ - deps: make(map[kubernetes.ObjectKind]map[string]client.Object), - owner: owner, + deps: make(map[kubernetes.ObjectKind]map[string]client.Object), + owner: owner, + componentAnnotations: make(map[v2alpha1.ComponentName]map[string]string), } if options != nil { store.supportCilium = options.SupportCilium @@ -75,6 +78,8 @@ type Store struct { platformInfo kubernetes.PlatformInfo isDDAControllerStore bool + componentAnnotations map[v2alpha1.ComponentName]map[string]string + scheme *runtime.Scheme logger logr.Logger owner metav1.Object @@ -208,8 +213,8 @@ func (ds *Store) Delete(kind kubernetes.ObjectKind, namespace string, name strin // Apply use to create/update resources in the api-server func (ds *Store) Apply(ctx context.Context, k8sClient client.Client) []error { - ds.mutex.RLock() - defer ds.mutex.RUnlock() + ds.mutex.Lock() + defer ds.mutex.Unlock() var errs []error var objsToCreate []client.Object @@ -233,6 +238,9 @@ func (ds *Store) Apply(ctx context.Context, k8sClient client.Client) []error { continue } + // Update object in store after preprocessing for equality check + ds.deps[kind][objID] = objStore + if objAPIServer == nil { ds.logger.V(2).Info("store.store Add object to create", "obj.namespace", objStore.GetNamespace(), "obj.name", objStore.GetName(), "obj.kind", kind) objsToCreate = append(objsToCreate, objStore) @@ -465,3 +473,12 @@ func shouldSetOwnerReference(kind kubernetes.ObjectKind, objNamespace, ownerName return true } + +// GetComponentAnnotations retrieves all annotations for a component +// Returns a copy to prevent external modifications to internal state +func (ds *Store) GetComponentAnnotations(componentName v2alpha1.ComponentName) map[string]string { + ds.mutex.RLock() + defer ds.mutex.RUnlock() + + return maps.Clone(ds.componentAnnotations[componentName]) +} diff --git a/internal/controller/datadogagentinternal/component_clusteragent.go b/internal/controller/datadogagentinternal/component_clusteragent.go index b283e02e5..2d4bdc633 100644 --- a/internal/controller/datadogagentinternal/component_clusteragent.go +++ b/internal/controller/datadogagentinternal/component_clusteragent.go @@ -97,6 +97,12 @@ func (c *ClusterAgentComponent) Reconcile(ctx context.Context, params *Reconcile override.Deployment(deployment, componentOverride) } + // Apply MD5 hashes for ConfigMaps + cmAnnotations := params.ResourceManagers.Store().GetComponentAnnotations(c.Name()) + for key, value := range cmAnnotations { + podManagers.Annotation().AddAnnotation(key, value) + } + return c.reconciler.createOrUpdateDeployment(params.Logger, params.DDAI, deployment, params.Status, updateStatusV2WithClusterAgent) } diff --git a/internal/controller/datadogagentinternal/component_clusterchecksrunner.go b/internal/controller/datadogagentinternal/component_clusterchecksrunner.go index 9bbb42902..dd2b70578 100644 --- a/internal/controller/datadogagentinternal/component_clusterchecksrunner.go +++ b/internal/controller/datadogagentinternal/component_clusterchecksrunner.go @@ -100,6 +100,12 @@ func (c *ClusterChecksRunnerComponent) Reconcile(ctx context.Context, params *Re override.Deployment(deployment, componentOverride) } + // Apply MD5 hashes for ConfigMaps + cmAnnotations := params.ResourceManagers.Store().GetComponentAnnotations(c.Name()) + for key, value := range cmAnnotations { + podManagers.Annotation().AddAnnotation(key, value) + } + return c.reconciler.createOrUpdateDeployment(params.Logger, params.DDAI, deployment, params.Status, updateStatusV2WithClusterChecksRunner) } diff --git a/internal/controller/datadogagentinternal/controller_reconcile_agent.go b/internal/controller/datadogagentinternal/controller_reconcile_agent.go index 26ded1840..7417065c2 100644 --- a/internal/controller/datadogagentinternal/controller_reconcile_agent.go +++ b/internal/controller/datadogagentinternal/controller_reconcile_agent.go @@ -103,6 +103,12 @@ func (r *Reconciler) reconcileV2Agent(logger logr.Logger, requiredComponents fea return reconcile.Result{}, nil } + // Apply MD5 hashes for ConfigMaps + cmAnnotations := resourcesManager.Store().GetComponentAnnotations(datadoghqv2alpha1.NodeAgentComponentName) + for key, value := range cmAnnotations { + podManagers.Annotation().AddAnnotation(key, value) + } + return r.createOrUpdateExtendedDaemonset(daemonsetLogger, ddai, eds, newStatus, updateEDSStatusV2WithAgent) } @@ -161,6 +167,12 @@ func (r *Reconciler) reconcileV2Agent(logger logr.Logger, requiredComponents fea return reconcile.Result{}, nil } + // Apply MD5 hashes for ConfigMaps + cmAnnotations := resourcesManager.Store().GetComponentAnnotations(datadoghqv2alpha1.NodeAgentComponentName) + for key, value := range cmAnnotations { + podManagers.Annotation().AddAnnotation(key, value) + } + return r.createOrUpdateDaemonset(daemonsetLogger, ddai, daemonset, newStatus, updateDSStatusV2WithAgent) } diff --git a/pkg/constants/const.go b/pkg/constants/const.go index 5aa7b362c..df2ad5818 100644 --- a/pkg/constants/const.go +++ b/pkg/constants/const.go @@ -75,6 +75,10 @@ const ( MD5DDAIDeploymentAnnotationKey = "agent.datadoghq.com/ddaispechash" // MD5ChecksumAnnotationKey annotation key is used to identify customConfig configurations MD5ChecksumAnnotationKey = "checksum/%s-custom-config" + // OperatorComponentLabelKey is used to identify the component of the resource + OperatorComponentLabelKeyPrefix = "operator.datadoghq.com/component." + // ConfigIDLabelKey stores a config ID for dependencies to generate a MD5 checksum annotation key + ConfigIDLabelKey = "operator.datadoghq.com/config-id" ) // Profiles diff --git a/pkg/constants/utils.go b/pkg/constants/utils.go index 6d41e0089..6543d733e 100644 --- a/pkg/constants/utils.go +++ b/pkg/constants/utils.go @@ -233,3 +233,7 @@ func GetDDAName(dda metav1.Object) string { } return dda.GetName() } + +func GetOperatorComponentLabelKey(component v2alpha1.ComponentName) string { + return OperatorComponentLabelKeyPrefix + string(component) +} diff --git a/pkg/kubernetes/const.go b/pkg/kubernetes/const.go index fb731a7b8..8755b5592 100644 --- a/pkg/kubernetes/const.go +++ b/pkg/kubernetes/const.go @@ -59,6 +59,8 @@ const ( ServicesKind = "services" // ValidatingWebhookConfigurationsKind is the ValidatingWebhookConfigurations resource kind ValidatingWebhookConfigurationsKind = "validatingwebhookconfigurations" + // ConfigMapsKind is the ConfigMaps resource kind + ConfigMapsKind = "configmaps" ) // getResourcesKind return the list of all possible ObjectKind supported as DatadogAgent dependencies @@ -77,6 +79,7 @@ func getResourcesKind(withCiliumResources bool) []ObjectKind { ServiceAccountsKind, ServicesKind, ValidatingWebhookConfigurationsKind, + ConfigMapsKind, } if withCiliumResources {