From cd9ca08bf28934a06d41384bd008d13fcac3d72b Mon Sep 17 00:00:00 2001 From: Yang Song Date: Mon, 22 Dec 2025 21:54:13 -0500 Subject: [PATCH 1/4] [AGENTONB-2593] DDOT gateway with interface component --- .../v1alpha1/datadogagentinternal_types.go | 3 + .../v1alpha1/zz_generated.deepcopy.go | 5 + .../v1alpha1/zz_generated.openapi.go | 6 + api/datadoghq/v2alpha1/datadogagent_types.go | 5 + .../v2alpha1/zz_generated.deepcopy.go | 5 + .../v2alpha1/zz_generated.openapi.go | 6 + .../datadoghq.com_datadogagentinternals.yaml | 48 +++++++ ...hq.com_datadogagentinternals_v1alpha1.json | 57 ++++++++ .../bases/v1/datadoghq.com_datadogagents.yaml | 48 +++++++ .../datadoghq.com_datadogagents_v2alpha1.json | 57 ++++++++ ...tadog-agent-with-otelcollectorgateway.yaml | 26 ++++ .../controller/datadogagent/common/const.go | 2 + .../controller/datadogagent/common/utils.go | 7 + .../component/otelagentgateway/const.go | 12 ++ .../component/otelagentgateway/default.go | 89 ++++++++++++ .../component/otelagentgateway/rbac.go | 19 +++ .../component_otelcollectorgateway.go | 128 +++++++++++++++++ .../controller/datadogagent/controller.go | 1 + .../controller_reconcile_v2_helpers.go | 3 + .../datadogagent/controller_v2_test.go | 5 +- .../feature/otelagentgateway/feature.go | 70 ++++++++++ .../datadogagent/feature/test/testsuite.go | 7 + .../datadogagent/global/dependencies.go | 24 ++++ .../global/otelcollectorgateway.go | 31 +++++ internal/controller/datadogagent/profile.go | 3 +- .../controller/datadogagent/profile_test.go | 8 +- .../component_otelcollectorgateway.go | 129 ++++++++++++++++++ pkg/constants/const.go | 2 + pkg/constants/utils.go | 14 ++ pkg/testutils/builder.go | 17 ++- 30 files changed, 832 insertions(+), 5 deletions(-) create mode 100644 examples/datadogagent/datadog-agent-with-otelcollectorgateway.yaml create mode 100644 internal/controller/datadogagent/component/otelagentgateway/const.go create mode 100644 internal/controller/datadogagent/component/otelagentgateway/default.go create mode 100644 internal/controller/datadogagent/component/otelagentgateway/rbac.go create mode 100644 internal/controller/datadogagent/component_otelcollectorgateway.go create mode 100644 internal/controller/datadogagent/feature/otelagentgateway/feature.go create mode 100644 internal/controller/datadogagent/global/otelcollectorgateway.go create mode 100644 internal/controller/datadogagentinternal/component_otelcollectorgateway.go diff --git a/api/datadoghq/v1alpha1/datadogagentinternal_types.go b/api/datadoghq/v1alpha1/datadogagentinternal_types.go index 747723fa20..a696468cdd 100644 --- a/api/datadoghq/v1alpha1/datadogagentinternal_types.go +++ b/api/datadoghq/v1alpha1/datadogagentinternal_types.go @@ -28,6 +28,9 @@ type DatadogAgentInternalStatus struct { // The actual state of the Cluster Checks Runner as a deployment. // +optional ClusterChecksRunner *v2alpha1.DeploymentStatus `json:"clusterChecksRunner,omitempty"` + // The actual state of the OTel Agent Gateway as a deployment. + // +optional + OtelAgentGateway *v2alpha1.DeploymentStatus `json:"otelAgentGateway,omitempty"` // RemoteConfigConfiguration stores the configuration received from RemoteConfig. // +optional RemoteConfigConfiguration *v2alpha1.RemoteConfigConfiguration `json:"remoteConfigConfiguration,omitempty"` diff --git a/api/datadoghq/v1alpha1/zz_generated.deepcopy.go b/api/datadoghq/v1alpha1/zz_generated.deepcopy.go index 6f28083eaf..bd1f581914 100644 --- a/api/datadoghq/v1alpha1/zz_generated.deepcopy.go +++ b/api/datadoghq/v1alpha1/zz_generated.deepcopy.go @@ -207,6 +207,11 @@ func (in *DatadogAgentInternalStatus) DeepCopyInto(out *DatadogAgentInternalStat *out = new(v2alpha1.DeploymentStatus) (*in).DeepCopyInto(*out) } + if in.OtelAgentGateway != nil { + in, out := &in.OtelAgentGateway, &out.OtelAgentGateway + *out = new(v2alpha1.DeploymentStatus) + (*in).DeepCopyInto(*out) + } if in.RemoteConfigConfiguration != nil { in, out := &in.RemoteConfigConfiguration, &out.RemoteConfigConfiguration *out = new(v2alpha1.RemoteConfigConfiguration) diff --git a/api/datadoghq/v1alpha1/zz_generated.openapi.go b/api/datadoghq/v1alpha1/zz_generated.openapi.go index 5482bce229..768486a6ef 100644 --- a/api/datadoghq/v1alpha1/zz_generated.openapi.go +++ b/api/datadoghq/v1alpha1/zz_generated.openapi.go @@ -351,6 +351,12 @@ func schema_datadog_operator_api_datadoghq_v1alpha1_DatadogAgentInternalStatus(r Ref: ref("github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1.DeploymentStatus"), }, }, + "otelAgentGateway": { + SchemaProps: spec.SchemaProps{ + Description: "The actual state of the OTel Agent Gateway as a deployment.", + Ref: ref("github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1.DeploymentStatus"), + }, + }, "remoteConfigConfiguration": { SchemaProps: spec.SchemaProps{ Description: "RemoteConfigConfiguration stores the configuration received from RemoteConfig.", diff --git a/api/datadoghq/v2alpha1/datadogagent_types.go b/api/datadoghq/v2alpha1/datadogagent_types.go index 9dbff3b7eb..366ab0ec7a 100644 --- a/api/datadoghq/v2alpha1/datadogagent_types.go +++ b/api/datadoghq/v2alpha1/datadogagent_types.go @@ -22,6 +22,8 @@ const ( ClusterAgentComponentName ComponentName = "clusterAgent" // ClusterChecksRunnerComponentName is the name of the Cluster Check Runner ClusterChecksRunnerComponentName ComponentName = "clusterChecksRunner" + // OtelAgentGatewayComponentName is the name of the OTel Agent Gateway + OtelAgentGatewayComponentName ComponentName = "otelAgentGateway" ) // DatadogAgentSpec defines the desired state of DatadogAgent @@ -2245,6 +2247,9 @@ type DatadogAgentStatus struct { // The actual state of the Cluster Checks Runner as a deployment. // +optional ClusterChecksRunner *DeploymentStatus `json:"clusterChecksRunner,omitempty"` + // The actual state of the OTel Agent Gateway as a deployment. + // +optional + OtelAgentGateway *DeploymentStatus `json:"otelAgentGateway,omitempty"` // RemoteConfigConfiguration stores the configuration received from RemoteConfig. // +optional RemoteConfigConfiguration *RemoteConfigConfiguration `json:"remoteConfigConfiguration,omitempty"` diff --git a/api/datadoghq/v2alpha1/zz_generated.deepcopy.go b/api/datadoghq/v2alpha1/zz_generated.deepcopy.go index 49a8cf9bda..d2afb1cdd0 100644 --- a/api/datadoghq/v2alpha1/zz_generated.deepcopy.go +++ b/api/datadoghq/v2alpha1/zz_generated.deepcopy.go @@ -1241,6 +1241,11 @@ func (in *DatadogAgentStatus) DeepCopyInto(out *DatadogAgentStatus) { *out = new(DeploymentStatus) (*in).DeepCopyInto(*out) } + if in.OtelAgentGateway != nil { + in, out := &in.OtelAgentGateway, &out.OtelAgentGateway + *out = new(DeploymentStatus) + (*in).DeepCopyInto(*out) + } if in.RemoteConfigConfiguration != nil { in, out := &in.RemoteConfigConfiguration, &out.RemoteConfigConfiguration *out = new(RemoteConfigConfiguration) diff --git a/api/datadoghq/v2alpha1/zz_generated.openapi.go b/api/datadoghq/v2alpha1/zz_generated.openapi.go index d9ec88e1cc..cc9c36aa56 100644 --- a/api/datadoghq/v2alpha1/zz_generated.openapi.go +++ b/api/datadoghq/v2alpha1/zz_generated.openapi.go @@ -583,6 +583,12 @@ func schema_datadog_operator_api_datadoghq_v2alpha1_DatadogAgentStatus(ref commo Ref: ref("github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1.DeploymentStatus"), }, }, + "otelAgentGateway": { + SchemaProps: spec.SchemaProps{ + Description: "The actual state of the OTel Agent Gateway as a deployment.", + Ref: ref("github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1.DeploymentStatus"), + }, + }, "remoteConfigConfiguration": { SchemaProps: spec.SchemaProps{ Description: "RemoteConfigConfiguration stores the configuration received from RemoteConfig.", diff --git a/config/crd/bases/v1/datadoghq.com_datadogagentinternals.yaml b/config/crd/bases/v1/datadoghq.com_datadogagentinternals.yaml index 0ace5baf45..81b6c5243d 100644 --- a/config/crd/bases/v1/datadoghq.com_datadogagentinternals.yaml +++ b/config/crd/bases/v1/datadoghq.com_datadogagentinternals.yaml @@ -7983,6 +7983,54 @@ spec: x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map + otelAgentGateway: + description: The actual state of the OTel Agent Gateway as a deployment. + properties: + availableReplicas: + description: Total number of available pods (ready for at least minReadySeconds) targeted by this Deployment. + format: int32 + type: integer + currentHash: + description: CurrentHash is the stored hash of the Deployment. + type: string + deploymentName: + description: DeploymentName corresponds to the name of the Deployment. + type: string + generatedToken: + description: |- + GeneratedToken corresponds to the generated token if any token was provided in the Credential configuration when ClusterAgent is + enabled. + type: string + lastUpdate: + description: LastUpdate is the last time the status was updated. + format: date-time + type: string + readyReplicas: + description: Total number of ready pods targeted by this Deployment. + format: int32 + type: integer + replicas: + description: Total number of non-terminated pods targeted by this Deployment (their labels match the selector). + format: int32 + type: integer + state: + description: State corresponds to the Deployment state. + type: string + status: + description: Status corresponds to the Deployment computed status. + type: string + unavailableReplicas: + description: |- + Total number of unavailable pods targeted by this Deployment. This is the total number of + pods that are still required for the Deployment to have 100% available capacity. They may + either be pods that are running but not yet available or pods that still have not been created. + format: int32 + type: integer + updatedReplicas: + description: Total number of non-terminated pods targeted by this Deployment that have the desired template spec. + format: int32 + type: integer + type: object remoteConfigConfiguration: description: RemoteConfigConfiguration stores the configuration received from RemoteConfig. properties: diff --git a/config/crd/bases/v1/datadoghq.com_datadogagentinternals_v1alpha1.json b/config/crd/bases/v1/datadoghq.com_datadogagentinternals_v1alpha1.json index cd8a1e08ef..2b17fab637 100644 --- a/config/crd/bases/v1/datadoghq.com_datadogagentinternals_v1alpha1.json +++ b/config/crd/bases/v1/datadoghq.com_datadogagentinternals_v1alpha1.json @@ -7797,6 +7797,63 @@ ], "x-kubernetes-list-type": "map" }, + "otelAgentGateway": { + "additionalProperties": false, + "description": "The actual state of the OTel Agent Gateway as a deployment.", + "properties": { + "availableReplicas": { + "description": "Total number of available pods (ready for at least minReadySeconds) targeted by this Deployment.", + "format": "int32", + "type": "integer" + }, + "currentHash": { + "description": "CurrentHash is the stored hash of the Deployment.", + "type": "string" + }, + "deploymentName": { + "description": "DeploymentName corresponds to the name of the Deployment.", + "type": "string" + }, + "generatedToken": { + "description": "GeneratedToken corresponds to the generated token if any token was provided in the Credential configuration when ClusterAgent is\nenabled.", + "type": "string" + }, + "lastUpdate": { + "description": "LastUpdate is the last time the status was updated.", + "format": "date-time", + "type": "string" + }, + "readyReplicas": { + "description": "Total number of ready pods targeted by this Deployment.", + "format": "int32", + "type": "integer" + }, + "replicas": { + "description": "Total number of non-terminated pods targeted by this Deployment (their labels match the selector).", + "format": "int32", + "type": "integer" + }, + "state": { + "description": "State corresponds to the Deployment state.", + "type": "string" + }, + "status": { + "description": "Status corresponds to the Deployment computed status.", + "type": "string" + }, + "unavailableReplicas": { + "description": "Total number of unavailable pods targeted by this Deployment. This is the total number of\npods that are still required for the Deployment to have 100% available capacity. They may\neither be pods that are running but not yet available or pods that still have not been created.", + "format": "int32", + "type": "integer" + }, + "updatedReplicas": { + "description": "Total number of non-terminated pods targeted by this Deployment that have the desired template spec.", + "format": "int32", + "type": "integer" + } + }, + "type": "object" + }, "remoteConfigConfiguration": { "additionalProperties": false, "description": "RemoteConfigConfiguration stores the configuration received from RemoteConfig.", diff --git a/config/crd/bases/v1/datadoghq.com_datadogagents.yaml b/config/crd/bases/v1/datadoghq.com_datadogagents.yaml index 2988b1c8a7..0cb03d29b9 100644 --- a/config/crd/bases/v1/datadoghq.com_datadogagents.yaml +++ b/config/crd/bases/v1/datadoghq.com_datadogagents.yaml @@ -8033,6 +8033,54 @@ spec: x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map + otelAgentGateway: + description: The actual state of the OTel Agent Gateway as a deployment. + properties: + availableReplicas: + description: Total number of available pods (ready for at least minReadySeconds) targeted by this Deployment. + format: int32 + type: integer + currentHash: + description: CurrentHash is the stored hash of the Deployment. + type: string + deploymentName: + description: DeploymentName corresponds to the name of the Deployment. + type: string + generatedToken: + description: |- + GeneratedToken corresponds to the generated token if any token was provided in the Credential configuration when ClusterAgent is + enabled. + type: string + lastUpdate: + description: LastUpdate is the last time the status was updated. + format: date-time + type: string + readyReplicas: + description: Total number of ready pods targeted by this Deployment. + format: int32 + type: integer + replicas: + description: Total number of non-terminated pods targeted by this Deployment (their labels match the selector). + format: int32 + type: integer + state: + description: State corresponds to the Deployment state. + type: string + status: + description: Status corresponds to the Deployment computed status. + type: string + unavailableReplicas: + description: |- + Total number of unavailable pods targeted by this Deployment. This is the total number of + pods that are still required for the Deployment to have 100% available capacity. They may + either be pods that are running but not yet available or pods that still have not been created. + format: int32 + type: integer + updatedReplicas: + description: Total number of non-terminated pods targeted by this Deployment that have the desired template spec. + format: int32 + type: integer + type: object remoteConfigConfiguration: description: RemoteConfigConfiguration stores the configuration received from RemoteConfig. properties: diff --git a/config/crd/bases/v1/datadoghq.com_datadogagents_v2alpha1.json b/config/crd/bases/v1/datadoghq.com_datadogagents_v2alpha1.json index 7d183724c3..abce9af160 100644 --- a/config/crd/bases/v1/datadoghq.com_datadogagents_v2alpha1.json +++ b/config/crd/bases/v1/datadoghq.com_datadogagents_v2alpha1.json @@ -7862,6 +7862,63 @@ ], "x-kubernetes-list-type": "map" }, + "otelAgentGateway": { + "additionalProperties": false, + "description": "The actual state of the OTel Agent Gateway as a deployment.", + "properties": { + "availableReplicas": { + "description": "Total number of available pods (ready for at least minReadySeconds) targeted by this Deployment.", + "format": "int32", + "type": "integer" + }, + "currentHash": { + "description": "CurrentHash is the stored hash of the Deployment.", + "type": "string" + }, + "deploymentName": { + "description": "DeploymentName corresponds to the name of the Deployment.", + "type": "string" + }, + "generatedToken": { + "description": "GeneratedToken corresponds to the generated token if any token was provided in the Credential configuration when ClusterAgent is\nenabled.", + "type": "string" + }, + "lastUpdate": { + "description": "LastUpdate is the last time the status was updated.", + "format": "date-time", + "type": "string" + }, + "readyReplicas": { + "description": "Total number of ready pods targeted by this Deployment.", + "format": "int32", + "type": "integer" + }, + "replicas": { + "description": "Total number of non-terminated pods targeted by this Deployment (their labels match the selector).", + "format": "int32", + "type": "integer" + }, + "state": { + "description": "State corresponds to the Deployment state.", + "type": "string" + }, + "status": { + "description": "Status corresponds to the Deployment computed status.", + "type": "string" + }, + "unavailableReplicas": { + "description": "Total number of unavailable pods targeted by this Deployment. This is the total number of\npods that are still required for the Deployment to have 100% available capacity. They may\neither be pods that are running but not yet available or pods that still have not been created.", + "format": "int32", + "type": "integer" + }, + "updatedReplicas": { + "description": "Total number of non-terminated pods targeted by this Deployment that have the desired template spec.", + "format": "int32", + "type": "integer" + } + }, + "type": "object" + }, "remoteConfigConfiguration": { "additionalProperties": false, "description": "RemoteConfigConfiguration stores the configuration received from RemoteConfig.", diff --git a/examples/datadogagent/datadog-agent-with-otelcollectorgateway.yaml b/examples/datadogagent/datadog-agent-with-otelcollectorgateway.yaml new file mode 100644 index 0000000000..4a5863a4fc --- /dev/null +++ b/examples/datadogagent/datadog-agent-with-otelcollectorgateway.yaml @@ -0,0 +1,26 @@ +# datadog-agent-with-otelagentgateway.yaml +# This example shows how to enable the OTel Agent Gateway component +# The OTel Agent Gateway runs as a separate deployment and can be used +# to collect and forward OpenTelemetry data. + +apiVersion: datadoghq.com/v2alpha1 +kind: DatadogAgent +metadata: + name: datadog +spec: + global: + credentials: + apiSecret: + secretName: datadog-secret + keyName: api-key + + features: + # Enable OTel Agent Gateway + otelAgentGateway: + enabled: true + + override: + # Optional: Configure the OTel Agent Gateway deployment + otelAgentGateway: + # Number of replicas for the gateway + replicas: 2 diff --git a/internal/controller/datadogagent/common/const.go b/internal/controller/datadogagent/common/const.go index 42f2492d3b..8a6ea319ec 100644 --- a/internal/controller/datadogagent/common/const.go +++ b/internal/controller/datadogagent/common/const.go @@ -49,6 +49,8 @@ const ( AgentReconcileConditionType = "AgentReconcile" // ClusterChecksRunnerReconcileConditionType ReconcileConditionType for Cluster Checks Runner component ClusterChecksRunnerReconcileConditionType = "ClusterChecksRunnerReconcile" + // OtelAgentGatewayReconcileConditionType ReconcileConditionType for OTel Agent Gateway component + OtelAgentGatewayReconcileConditionType = "OtelAgentGatewayReconcile" // OverrideReconcileConflictConditionType ReconcileConditionType for override conflict OverrideReconcileConflictConditionType = "OverrideReconcileConflict" // DatadogAgentReconcileErrorConditionType ReconcileConditionType for DatadogAgent reconcile error diff --git a/internal/controller/datadogagent/common/utils.go b/internal/controller/datadogagent/common/utils.go index 2e060480d4..596a8537f9 100644 --- a/internal/controller/datadogagent/common/utils.go +++ b/internal/controller/datadogagent/common/utils.go @@ -136,6 +136,13 @@ func GetAgentLocalServiceSelector(dda metav1.Object) map[string]string { } } +func GetOtelAgentGatewayServiceSelector(dda metav1.Object) map[string]string { + return map[string]string{ + kubernetes.AppKubernetesPartOfLabelKey: object.NewPartOfLabelValue(dda).String(), + apicommon.AgentDeploymentComponentLabelKey: constants.DefaultOtelAgentGatewayResourceSuffix, + } +} + // ShouldCreateAgentLocalService returns whether the node agent local service should be created based on the Kubernetes version func ShouldCreateAgentLocalService(versionInfo *version.Info, forceEnableLocalService bool) bool { if versionInfo == nil || versionInfo.GitVersion == "" { diff --git a/internal/controller/datadogagent/component/otelagentgateway/const.go b/internal/controller/datadogagent/component/otelagentgateway/const.go new file mode 100644 index 0000000000..5e71d3bbf3 --- /dev/null +++ b/internal/controller/datadogagent/component/otelagentgateway/const.go @@ -0,0 +1,12 @@ +// 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 otelagentgateway + +const ( + // pdbMaxUnavailableInstances = 1 + // DefaultOtelAgentGatewayReplicas default OTel Agent Gateway deployment replicas + defaultOtelAgentGatewayReplicas = 1 +) diff --git a/internal/controller/datadogagent/component/otelagentgateway/default.go b/internal/controller/datadogagent/component/otelagentgateway/default.go new file mode 100644 index 0000000000..2edeb6051c --- /dev/null +++ b/internal/controller/datadogagent/component/otelagentgateway/default.go @@ -0,0 +1,89 @@ +// 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 ( + "fmt" + "maps" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + apicommon "github.com/DataDog/datadog-operator/api/datadoghq/common" + apiutils "github.com/DataDog/datadog-operator/api/utils" + "github.com/DataDog/datadog-operator/internal/controller/datadogagent/common" + "github.com/DataDog/datadog-operator/pkg/constants" + "github.com/DataDog/datadog-operator/pkg/images" +) + +// GetOtelAgentGatewayName return the OtelAgentGateway name based on the DatadogAgent name +func GetOtelAgentGatewayName(dda metav1.Object) string { + return fmt.Sprintf("%s-%s", dda.GetName(), constants.DefaultOtelAgentGatewayResourceSuffix) +} + +// GetOtelAgentGatewayRbacResourcesName returns the OtelAgentGateway RBAC resource name +func GetOtelAgentGatewayRbacResourcesName(dda metav1.Object) string { + return fmt.Sprintf("%s-%s", dda.GetName(), constants.DefaultOtelAgentGatewayResourceSuffix) +} + +// NewDefaultOtelAgentGatewayDeployment return a new default otel-collector-gateway deployment +func NewDefaultOtelAgentGatewayDeployment(dda metav1.Object) *appsv1.Deployment { + deployment := common.NewDeployment(dda, constants.DefaultOtelAgentGatewayResourceSuffix, GetOtelAgentGatewayName(dda), common.GetAgentVersion(dda), nil) + + podTemplate := NewDefaultOtelAgentGatewayPodTemplateSpec(dda) + maps.Copy(podTemplate.Labels, deployment.GetLabels()) + maps.Copy(podTemplate.Annotations, deployment.GetAnnotations()) + + deployment.Spec.Template = *podTemplate + deployment.Spec.Replicas = apiutils.NewInt32Pointer(defaultOtelAgentGatewayReplicas) + + return deployment +} + +// NewDefaultOtelAgentGatewayPodTemplateSpec returns a default otel-collector-gateway pod template spec +func NewDefaultOtelAgentGatewayPodTemplateSpec(dda metav1.Object) *corev1.PodTemplateSpec { + template := &corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: make(map[string]string), + Annotations: make(map[string]string), + }, + Spec: defaultPodSpec(dda), + } + + return template +} + +func defaultPodSpec(dda metav1.Object) corev1.PodSpec { + return corev1.PodSpec{ + ServiceAccountName: GetOtelAgentGatewayRbacResourcesName(dda), + Containers: []corev1.Container{ + { + Name: string(apicommon.OtelAgent), + Image: images.GetLatestDdotCollectorImage(), + Command: []string{"otel-agent", "--sync-delay=30s"}, + VolumeMounts: []corev1.VolumeMount{ + common.GetVolumeMountForLogs(), + }, + Ports: []corev1.ContainerPort{ + { + Name: "otel-grpc", + ContainerPort: 4317, + Protocol: corev1.ProtocolTCP, + }, + { + Name: "otel-http", + ContainerPort: 4318, + Protocol: corev1.ProtocolTCP, + }, + }, + }, + }, + Volumes: []corev1.Volume{ + common.GetVolumeForLogs(), + }, + } +} diff --git a/internal/controller/datadogagent/component/otelagentgateway/rbac.go b/internal/controller/datadogagent/component/otelagentgateway/rbac.go new file mode 100644 index 0000000000..535a10ae78 --- /dev/null +++ b/internal/controller/datadogagent/component/otelagentgateway/rbac.go @@ -0,0 +1,19 @@ +// 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 otelagentgateway + +import ( + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// RBAC for OTel Agent Gateway + +// GetDefaultOtelAgentGatewayClusterRolePolicyRules returns the default Cluster Role Policy Rules for the OTel Agent Gateway +func GetDefaultOtelAgentGatewayClusterRolePolicyRules(dda metav1.Object, excludeNonResourceRules bool) []rbacv1.PolicyRule { + // TODO(OTAGENT-512): add RBAC for OTel load balancing exporter & k8s attributes processor + return []rbacv1.PolicyRule{} +} diff --git a/internal/controller/datadogagent/component_otelcollectorgateway.go b/internal/controller/datadogagent/component_otelcollectorgateway.go new file mode 100644 index 0000000000..3ee16a8f82 --- /dev/null +++ b/internal/controller/datadogagent/component_otelcollectorgateway.go @@ -0,0 +1,128 @@ +// 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 datadogagent + +import ( + "context" + + "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" + + datadoghqv2alpha1 "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" + apiutils "github.com/DataDog/datadog-operator/api/utils" + "github.com/DataDog/datadog-operator/internal/controller/datadogagent/common" + componentotelagentgateway "github.com/DataDog/datadog-operator/internal/controller/datadogagent/component/otelagentgateway" + "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature" + "github.com/DataDog/datadog-operator/internal/controller/datadogagent/global" + "github.com/DataDog/datadog-operator/internal/controller/datadogagent/override" + "github.com/DataDog/datadog-operator/pkg/condition" + "github.com/DataDog/datadog-operator/pkg/controller/utils/datadog" + "github.com/DataDog/datadog-operator/pkg/kubernetes" +) + +// OtelAgentGatewayComponent implements ComponentReconciler for the OTel Agent Gateway deployment +type OtelAgentGatewayComponent struct { + reconciler *Reconciler +} + +// NewOtelAgentGatewayComponent creates a new OtelAgentGateway component +func NewOtelAgentGatewayComponent(reconciler *Reconciler) *OtelAgentGatewayComponent { + return &OtelAgentGatewayComponent{ + reconciler: reconciler, + } +} + +// Name returns the component name +func (c *OtelAgentGatewayComponent) Name() datadoghqv2alpha1.ComponentName { + return datadoghqv2alpha1.OtelAgentGatewayComponentName +} + +// IsEnabled checks if the OtelAgentGateway component should be reconciled +func (c *OtelAgentGatewayComponent) IsEnabled(requiredComponents feature.RequiredComponents) bool { + return requiredComponents.OtelAgentGateway.IsEnabled() +} + +// GetConditionType returns the condition type for status updates +func (c *OtelAgentGatewayComponent) GetConditionType() string { + return common.OtelAgentGatewayReconcileConditionType +} + +// Reconcile reconciles the OtelAgentGateway component +func (c *OtelAgentGatewayComponent) Reconcile(ctx context.Context, params *ReconcileComponentParams) (reconcile.Result, error) { + var result reconcile.Result + + // Start by creating the Default OtelAgentGateway deployment + deployment := componentotelagentgateway.NewDefaultOtelAgentGatewayDeployment(params.DDA) + podManagers := feature.NewPodTemplateManagers(&deployment.Spec.Template) + + // Set Global setting on the default deployment + global.ApplyGlobalSettingsOtelAgentGateway(params.Logger, podManagers, params.DDA.GetObjectMeta(), ¶ms.DDA.Spec, params.ResourceManagers, params.RequiredComponents) + + // Apply features changes on the Deployment.Spec.Template + for _, feat := range params.Features { + if errFeat := feat.ManageOtelAgentGateway(podManagers, ""); errFeat != nil { + return result, errFeat + } + } + + // If Override is defined for the OtelAgentGateway component, apply the override on the PodTemplateSpec + if componentOverride, ok := params.DDA.Spec.Override[c.Name()]; ok { + if apiutils.BoolValue(componentOverride.Disabled) { + // This case is handled by the registry, but we double-check here + return c.Cleanup(ctx, params) + } + override.PodTemplateSpec(params.Logger, podManagers, componentOverride, c.Name(), params.DDA.Name) + override.Deployment(deployment, componentOverride) + } + + return c.reconciler.createOrUpdateDeployment(params.Logger, params.DDA, deployment, params.Status, updateStatusV2WithOtelAgentGateway) +} + +// Cleanup removes the OtelAgentGateway deployment +func (c *OtelAgentGatewayComponent) Cleanup(ctx context.Context, params *ReconcileComponentParams) (reconcile.Result, error) { + deployment := componentotelagentgateway.NewDefaultOtelAgentGatewayDeployment(params.DDA) + return c.reconciler.cleanupV2OtelAgentGateway(params.Logger, params.DDA, deployment, params.Status) +} + +func (r *Reconciler) cleanupV2OtelAgentGateway(logger logr.Logger, dda *datadoghqv2alpha1.DatadogAgent, deployment *appsv1.Deployment, newStatus *datadoghqv2alpha1.DatadogAgentStatus) (reconcile.Result, error) { + nsName := types.NamespacedName{ + Name: deployment.GetName(), + Namespace: deployment.GetNamespace(), + } + + // OtelAgentGateway deployment attached to this instance + otelAgentGatewayDeployment := &appsv1.Deployment{} + if err := r.client.Get(context.TODO(), nsName, otelAgentGatewayDeployment); err != nil { + if !errors.IsNotFound(err) { + return reconcile.Result{}, err + } + } else { + logger.Info("Deleting OTel Agent Gateway Deployment", "deployment.Namespace", otelAgentGatewayDeployment.Namespace, "deployment.Name", otelAgentGatewayDeployment.Name) + event := buildEventInfo(otelAgentGatewayDeployment.Name, otelAgentGatewayDeployment.Namespace, kubernetes.DeploymentKind, datadog.DeletionEvent) + r.recordEvent(dda, event) + if err := r.client.Delete(context.TODO(), otelAgentGatewayDeployment); err != nil { + return reconcile.Result{}, err + } + } + + deleteStatusWithOtelAgentGateway(newStatus) + + return reconcile.Result{}, nil +} + +func updateStatusV2WithOtelAgentGateway(deployment *appsv1.Deployment, newStatus *datadoghqv2alpha1.DatadogAgentStatus, updateTime metav1.Time, status metav1.ConditionStatus, reason, message string) { + newStatus.OtelAgentGateway = condition.UpdateDeploymentStatus(deployment, newStatus.OtelAgentGateway, &updateTime) + condition.UpdateDatadogAgentStatusConditions(newStatus, updateTime, common.OtelAgentGatewayReconcileConditionType, status, reason, message, true) +} + +func deleteStatusWithOtelAgentGateway(newStatus *datadoghqv2alpha1.DatadogAgentStatus) { + newStatus.OtelAgentGateway = nil + condition.DeleteDatadogAgentStatusCondition(newStatus, common.OtelAgentGatewayReconcileConditionType) +} diff --git a/internal/controller/datadogagent/controller.go b/internal/controller/datadogagent/controller.go index 42d1fdffe2..095d3ee55a 100644 --- a/internal/controller/datadogagent/controller.go +++ b/internal/controller/datadogagent/controller.go @@ -47,6 +47,7 @@ import ( _ "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/npm" _ "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/oomkill" _ "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/orchestratorexplorer" + _ "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/otelagentgateway" _ "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/otelcollector" _ "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/otlp" _ "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/processdiscovery" diff --git a/internal/controller/datadogagent/controller_reconcile_v2_helpers.go b/internal/controller/datadogagent/controller_reconcile_v2_helpers.go index 030488c69e..dc62a8b52d 100644 --- a/internal/controller/datadogagent/controller_reconcile_v2_helpers.go +++ b/internal/controller/datadogagent/controller_reconcile_v2_helpers.go @@ -56,6 +56,9 @@ func (r *Reconciler) manageGlobalDependencies(logger logr.Logger, dda *datadoghq if err := global.ApplyGlobalComponentDependencies(logger, dda.GetObjectMeta(), &dda.Spec, &dda.Status, resourceManagers, datadoghqv2alpha1.ClusterChecksRunnerComponentName, requiredComponents.ClusterChecksRunner, false); len(err) > 0 { errs = append(errs, err...) } + if err := global.ApplyGlobalComponentDependencies(logger, dda.GetObjectMeta(), &dda.Spec, &dda.Status, resourceManagers, datadoghqv2alpha1.OtelAgentGatewayComponentName, requiredComponents.OtelAgentGateway, false); len(err) > 0 { + errs = append(errs, err...) + } if len(errs) > 0 { return errors.NewAggregate(errs) diff --git a/internal/controller/datadogagent/controller_v2_test.go b/internal/controller/datadogagent/controller_v2_test.go index 779002f2f4..a43daba5ec 100644 --- a/internal/controller/datadogagent/controller_v2_test.go +++ b/internal/controller/datadogagent/controller_v2_test.go @@ -1725,7 +1725,7 @@ func Test_DDAI_ReconcileV3(t *testing.T) { profileDDAI := getBaseDDAI(dda) profileDDAI.Name = "foo-profile" profileDDAI.Annotations = map[string]string{ - constants.MD5DDAIDeploymentAnnotationKey: "cc45afac2d101aad1984d1e05c2fc592", + constants.MD5DDAIDeploymentAnnotationKey: "74ddba33da89fb703cbe43718cb78e1e", } profileDDAI.Labels[constants.ProfileLabelKey] = "foo-profile" profileDDAI.Spec.Override = map[v2alpha1.ComponentName]*v2alpha1.DatadogAgentComponentOverride{ @@ -1735,6 +1735,9 @@ func Test_DDAI_ReconcileV3(t *testing.T) { v2alpha1.ClusterChecksRunnerComponentName: { Disabled: apiutils.NewBoolPointer(true), }, + v2alpha1.OtelAgentGatewayComponentName: { + Disabled: apiutils.NewBoolPointer(true), + }, v2alpha1.NodeAgentComponentName: { Name: apiutils.NewStringPointer("foo-profile-agent"), Labels: map[string]string{ diff --git a/internal/controller/datadogagent/feature/otelagentgateway/feature.go b/internal/controller/datadogagent/feature/otelagentgateway/feature.go new file mode 100644 index 0000000000..911d048930 --- /dev/null +++ b/internal/controller/datadogagent/feature/otelagentgateway/feature.go @@ -0,0 +1,70 @@ +// 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 ( + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" + "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature" +) + +func init() { + err := feature.Register(feature.OtelAgentGatewayIDType, buildOtelAgentGatewayFeature) + if err != nil { + panic(err) + } +} + +type otelAgentGatewayFeature struct { + logger logr.Logger +} + +func buildOtelAgentGatewayFeature(options *feature.Options) feature.Feature { + feature := &otelAgentGatewayFeature{} + if options != nil { + feature.logger = options.Logger + } + return feature +} + +// ID returns the ID of the Feature +func (f *otelAgentGatewayFeature) ID() feature.IDType { + return feature.OtelAgentGatewayIDType +} + +func (f *otelAgentGatewayFeature) Configure(dda metav1.Object, ddaSpec *v2alpha1.DatadogAgentSpec, _ *v2alpha1.RemoteConfigConfiguration) (reqComp feature.RequiredComponents) { + return reqComp +} + +func (f *otelAgentGatewayFeature) ManageDependencies(feature.ResourceManagers, string) error { + return nil +} + +func (f *otelAgentGatewayFeature) ManageClusterAgent(feature.PodTemplateManagers, string) error { + // OtelAgentGateway doesn't need to configure the Cluster Agent + return nil +} + +func (f *otelAgentGatewayFeature) ManageSingleContainerNodeAgent(feature.PodTemplateManagers, string) error { + // OtelAgentGateway doesn't need to configure the Node Agent + return nil +} + +func (f *otelAgentGatewayFeature) ManageNodeAgent(feature.PodTemplateManagers, string) error { + // OtelAgentGateway doesn't need to configure the Node Agent + return nil +} + +func (f *otelAgentGatewayFeature) ManageClusterChecksRunner(feature.PodTemplateManagers, string) error { + // OtelAgentGateway doesn't need to configure the Cluster Checks Runner + return nil +} + +func (f *otelAgentGatewayFeature) ManageOtelAgentGateway(feature.PodTemplateManagers, string) error { + return nil +} diff --git a/internal/controller/datadogagent/feature/test/testsuite.go b/internal/controller/datadogagent/feature/test/testsuite.go index 82db785ffa..497550b167 100644 --- a/internal/controller/datadogagent/feature/test/testsuite.go +++ b/internal/controller/datadogagent/feature/test/testsuite.go @@ -44,6 +44,7 @@ type FeatureTest struct { Agent *ComponentTest ClusterAgent *ComponentTest ClusterChecksRunner *ComponentTest + OtelAgentGateway *ComponentTest // Want WantConfigure bool WantManageDependenciesErr bool @@ -169,6 +170,12 @@ func runTest(t *testing.T, tt FeatureTest) { _ = feat.ManageClusterChecksRunner(tplManager, "") tt.ClusterChecksRunner.WantFunc(t, tplManager) } + + if tt.OtelAgentGateway != nil { + tplManager, _ := tt.OtelAgentGateway.CreateFunc(t) + _ = feat.ManageOtelAgentGateway(tplManager, "") + tt.OtelAgentGateway.WantFunc(t, tplManager) + } } } diff --git a/internal/controller/datadogagent/global/dependencies.go b/internal/controller/datadogagent/global/dependencies.go index 6a4d4107d8..9c6a525e80 100644 --- a/internal/controller/datadogagent/global/dependencies.go +++ b/internal/controller/datadogagent/global/dependencies.go @@ -22,6 +22,7 @@ import ( "github.com/DataDog/datadog-operator/internal/controller/datadogagent/component/clusteragent" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/component/clusterchecksrunner" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/component/objects" + otelagentgateway "github.com/DataDog/datadog-operator/internal/controller/datadogagent/component/otelagentgateway" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature" featureutils "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/utils" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/object" @@ -263,6 +264,8 @@ func rbacDependencies(ddaMeta metav1.Object, ddaSpec *v2alpha1.DatadogAgentSpec, return nodeAgentDependencies(ddaMeta, ddaSpec, manager) case v2alpha1.ClusterChecksRunnerComponentName: return clusterChecksRunnerDependencies(ddaMeta, ddaSpec, manager) + case v2alpha1.OtelAgentGatewayComponentName: + return otelAgentGatewayDependencies(ddaMeta, ddaSpec, manager) } return nil @@ -337,6 +340,24 @@ func clusterChecksRunnerDependencies(ddaMeta metav1.Object, ddaSpec *v2alpha1.Da return errors.Join(errs...) } +func otelAgentGatewayDependencies(ddaMeta metav1.Object, ddaSpec *v2alpha1.DatadogAgentSpec, manager feature.ResourceManagers) error { + var errs []error + serviceAccountName := constants.GetOtelAgentGatewayServiceAccount(ddaMeta.GetName(), ddaSpec) + rbacResourcesName := otelagentgateway.GetOtelAgentGatewayRbacResourcesName(ddaMeta) + + // Service account + if err := manager.RBACManager().AddServiceAccountByComponent(ddaMeta.GetNamespace(), serviceAccountName, string(v2alpha1.OtelAgentGatewayComponentName)); err != nil { + errs = append(errs, err) + } + + // ClusterRole creation + if err := manager.RBACManager().AddClusterPolicyRulesByComponent(ddaMeta.GetNamespace(), rbacResourcesName, serviceAccountName, otelagentgateway.GetDefaultOtelAgentGatewayClusterRolePolicyRules(ddaMeta, disableNonResourceRules(ddaSpec)), string(v2alpha1.OtelAgentGatewayComponentName)); err != nil { + errs = append(errs, err) + } + + return errors.Join(errs...) +} + func addNetworkPolicyDependencies(ddaMeta metav1.Object, ddaSpec *v2alpha1.DatadogAgentSpec, manager feature.ResourceManagers, componentName v2alpha1.ComponentName) error { config := ddaSpec.Global if enabled, flavor := constants.IsNetworkPolicyEnabled(ddaSpec); enabled { @@ -375,6 +396,9 @@ func addSecretBackendDependencies(logger logr.Logger, ddaMeta metav1.Object, dda componentSaName = constants.GetAgentServiceAccount(ddaMeta.GetName(), ddaSpec) case v2alpha1.ClusterChecksRunnerComponentName: componentSaName = constants.GetClusterChecksRunnerServiceAccount(ddaMeta.GetName(), ddaSpec) + // Do not use secretBackend global setting for OTel Agent Gateway + case v2alpha1.OtelAgentGatewayComponentName: + return nil } agentName := ddaMeta.GetName() diff --git a/internal/controller/datadogagent/global/otelcollectorgateway.go b/internal/controller/datadogagent/global/otelcollectorgateway.go new file mode 100644 index 0000000000..6bf5598b42 --- /dev/null +++ b/internal/controller/datadogagent/global/otelcollectorgateway.go @@ -0,0 +1,31 @@ +// 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 global + +import ( + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" + "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature" +) + +// ApplyGlobalSettingsOtelAgentGateway applies global settings to the OtelAgentGateway deployment +func ApplyGlobalSettingsOtelAgentGateway( + logger logr.Logger, + manager feature.PodTemplateManagers, + ddaMeta metav1.Object, + ddaSpec *v2alpha1.DatadogAgentSpec, + resourcesManager feature.ResourceManagers, + requiredComponents feature.RequiredComponents, +) { + applyGlobalSettings(logger, manager, ddaMeta, ddaSpec, resourcesManager, requiredComponents) + applyOtelAgentGatewayResources(manager, ddaSpec) +} + +func applyOtelAgentGatewayResources(manager feature.PodTemplateManagers, ddaSpec *v2alpha1.DatadogAgentSpec) { + // Add any OtelAgentGateway-specific resource configuration here +} diff --git a/internal/controller/datadogagent/profile.go b/internal/controller/datadogagent/profile.go index 859ac1b8a1..79a1bbce1c 100644 --- a/internal/controller/datadogagent/profile.go +++ b/internal/controller/datadogagent/profile.go @@ -210,9 +210,10 @@ func setProfileSpec(ddai *v1alpha1.DatadogAgentInternal, profile *v1alpha1.Datad affinity := setProfileDDAIAffinity(ddai, profile) if !agentprofile.IsDefaultProfile(profile.Namespace, profile.Name) { ddai.Spec = *profile.Spec.Config - // DCA and CCR are auto disabled for user created profiles + // DCA, CCR, and OtelAgentGateway are auto disabled for user created profiles disableComponent(ddai, v2alpha1.ClusterAgentComponentName) disableComponent(ddai, v2alpha1.ClusterChecksRunnerComponentName) + disableComponent(ddai, v2alpha1.OtelAgentGatewayComponentName) setProfileNodeAgentOverride(ddai, profile) } ensureOverrideExists(ddai, v2alpha1.NodeAgentComponentName) diff --git a/internal/controller/datadogagent/profile_test.go b/internal/controller/datadogagent/profile_test.go index 439bf08ec3..71895d7058 100644 --- a/internal/controller/datadogagent/profile_test.go +++ b/internal/controller/datadogagent/profile_test.go @@ -198,7 +198,7 @@ func Test_computeProfileMerge(t *testing.T) { Name: "foo-profile", Namespace: "bar", Annotations: map[string]string{ - constants.MD5DDAIDeploymentAnnotationKey: "7540aac2cb9cbb8adc8666a70fc3e822", + constants.MD5DDAIDeploymentAnnotationKey: "e160cdf078da13507876397e80bbe4e0", }, }, Spec: v2alpha1.DatadogAgentSpec{ @@ -265,6 +265,9 @@ func Test_computeProfileMerge(t *testing.T) { v2alpha1.ClusterChecksRunnerComponentName: { Disabled: apiutils.NewBoolPointer(true), }, + v2alpha1.OtelAgentGatewayComponentName: { + Disabled: apiutils.NewBoolPointer(true), + }, }, }, }, @@ -542,6 +545,9 @@ func Test_setProfileSpec(t *testing.T) { v2alpha1.ClusterChecksRunnerComponentName: { Disabled: apiutils.NewBoolPointer(true), }, + v2alpha1.OtelAgentGatewayComponentName: { + Disabled: apiutils.NewBoolPointer(true), + }, }, }, }, diff --git a/internal/controller/datadogagentinternal/component_otelcollectorgateway.go b/internal/controller/datadogagentinternal/component_otelcollectorgateway.go new file mode 100644 index 0000000000..00866359ca --- /dev/null +++ b/internal/controller/datadogagentinternal/component_otelcollectorgateway.go @@ -0,0 +1,129 @@ +// 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 datadogagentinternal + +import ( + "context" + + "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" + + "github.com/DataDog/datadog-operator/api/datadoghq/v1alpha1" + datadoghqv2alpha1 "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" + apiutils "github.com/DataDog/datadog-operator/api/utils" + "github.com/DataDog/datadog-operator/internal/controller/datadogagent/common" + componentotelagentgateway "github.com/DataDog/datadog-operator/internal/controller/datadogagent/component/otelagentgateway" + "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature" + "github.com/DataDog/datadog-operator/internal/controller/datadogagent/global" + "github.com/DataDog/datadog-operator/internal/controller/datadogagent/override" + "github.com/DataDog/datadog-operator/pkg/condition" + "github.com/DataDog/datadog-operator/pkg/controller/utils/datadog" + "github.com/DataDog/datadog-operator/pkg/kubernetes" +) + +// OtelAgentGatewayComponent implements ComponentReconciler for the OTel Agent Gateway deployment +type OtelAgentGatewayComponent struct { + reconciler *Reconciler +} + +// NewOtelAgentGatewayComponent creates a new OtelAgentGateway component +func NewOtelAgentGatewayComponent(reconciler *Reconciler) *OtelAgentGatewayComponent { + return &OtelAgentGatewayComponent{ + reconciler: reconciler, + } +} + +// Name returns the component name +func (c *OtelAgentGatewayComponent) Name() datadoghqv2alpha1.ComponentName { + return datadoghqv2alpha1.OtelAgentGatewayComponentName +} + +// IsEnabled checks if the OtelAgentGateway component should be reconciled +func (c *OtelAgentGatewayComponent) IsEnabled(requiredComponents feature.RequiredComponents) bool { + return requiredComponents.OtelAgentGateway.IsEnabled() +} + +// GetConditionType returns the condition type for status updates +func (c *OtelAgentGatewayComponent) GetConditionType() string { + return common.OtelAgentGatewayReconcileConditionType +} + +// Reconcile reconciles the OtelAgentGateway component +func (c *OtelAgentGatewayComponent) Reconcile(ctx context.Context, params *ReconcileComponentParams) (reconcile.Result, error) { + var result reconcile.Result + + // Start by creating the Default OtelAgentGateway deployment + deployment := componentotelagentgateway.NewDefaultOtelAgentGatewayDeployment(params.DDAI) + podManagers := feature.NewPodTemplateManagers(&deployment.Spec.Template) + + // Set Global setting on the default deployment + global.ApplyGlobalSettingsOtelAgentGateway(params.Logger, podManagers, params.DDAI.GetObjectMeta(), ¶ms.DDAI.Spec, params.ResourceManagers, params.RequiredComponents) + + // Apply features changes on the Deployment.Spec.Template + for _, feat := range params.Features { + if errFeat := feat.ManageOtelAgentGateway(podManagers, ""); errFeat != nil { + return result, errFeat + } + } + + // If Override is defined for the OtelAgentGateway component, apply the override on the PodTemplateSpec + if componentOverride, ok := params.DDAI.Spec.Override[c.Name()]; ok { + if apiutils.BoolValue(componentOverride.Disabled) { + // This case is handled by the registry, but we double-check here + return c.Cleanup(ctx, params) + } + override.PodTemplateSpec(params.Logger, podManagers, componentOverride, c.Name(), params.DDAI.Name) + override.Deployment(deployment, componentOverride) + } + + return c.reconciler.createOrUpdateDeployment(params.Logger, params.DDAI, deployment, params.Status, updateStatusV2WithOtelAgentGateway) +} + +// Cleanup removes the OtelAgentGateway deployment +func (c *OtelAgentGatewayComponent) Cleanup(ctx context.Context, params *ReconcileComponentParams) (reconcile.Result, error) { + deployment := componentotelagentgateway.NewDefaultOtelAgentGatewayDeployment(params.DDAI) + return c.reconciler.cleanupV2OtelAgentGateway(params.Logger, params.DDAI, deployment, params.Status) +} + +func (r *Reconciler) cleanupV2OtelAgentGateway(logger logr.Logger, ddai *v1alpha1.DatadogAgentInternal, deployment *appsv1.Deployment, newStatus *v1alpha1.DatadogAgentInternalStatus) (reconcile.Result, error) { + nsName := types.NamespacedName{ + Name: deployment.GetName(), + Namespace: deployment.GetNamespace(), + } + + // OtelAgentGateway deployment attached to this instance + otelAgentGatewayDeployment := &appsv1.Deployment{} + if err := r.client.Get(context.TODO(), nsName, otelAgentGatewayDeployment); err != nil { + if !errors.IsNotFound(err) { + return reconcile.Result{}, err + } + } else { + logger.Info("Deleting OTel Agent Gateway Deployment", "deployment.Namespace", otelAgentGatewayDeployment.Namespace, "deployment.Name", otelAgentGatewayDeployment.Name) + event := buildEventInfo(otelAgentGatewayDeployment.Name, otelAgentGatewayDeployment.Namespace, kubernetes.DeploymentKind, datadog.DeletionEvent) + r.recordEvent(ddai, event) + if err := r.client.Delete(context.TODO(), otelAgentGatewayDeployment); err != nil { + return reconcile.Result{}, err + } + } + + deleteStatusWithOtelAgentGateway(newStatus) + + return reconcile.Result{}, nil +} + +func updateStatusV2WithOtelAgentGateway(deployment *appsv1.Deployment, newStatus *v1alpha1.DatadogAgentInternalStatus, updateTime metav1.Time, status metav1.ConditionStatus, reason, message string) { + newStatus.OtelAgentGateway = condition.UpdateDeploymentStatus(deployment, newStatus.OtelAgentGateway, &updateTime) + condition.UpdateDatadogAgentInternalStatusConditions(newStatus, updateTime, common.OtelAgentGatewayReconcileConditionType, status, reason, message, true) +} + +func deleteStatusWithOtelAgentGateway(newStatus *v1alpha1.DatadogAgentInternalStatus) { + newStatus.OtelAgentGateway = nil + condition.DeleteDatadogAgentInternalStatusCondition(newStatus, common.OtelAgentGatewayReconcileConditionType) +} diff --git a/pkg/constants/const.go b/pkg/constants/const.go index 4f49316d2e..c79dfef899 100644 --- a/pkg/constants/const.go +++ b/pkg/constants/const.go @@ -59,6 +59,8 @@ const ( DefaultClusterAgentResourceSuffix = "cluster-agent" // DefaultClusterChecksRunnerResourceSuffix use as suffix for cluster-checks-runner resource naming DefaultClusterChecksRunnerResourceSuffix = "cluster-checks-runner" + // DefaultOtelAgentGatewayResourceSuffix use as suffix for otel-agent-gateway resource naming + DefaultOtelAgentGatewayResourceSuffix = "otel-agent-gateway" ) // Labels diff --git a/pkg/constants/utils.go b/pkg/constants/utils.go index ac49d2f3bc..d13afd1ac7 100644 --- a/pkg/constants/utils.go +++ b/pkg/constants/utils.go @@ -68,6 +68,15 @@ func GetClusterChecksRunnerServiceAccount(objName string, ddaSpec *v2alpha1.Data return saDefault } +// GetOtelAgentGatewayServiceAccount return the otel-collector-gateway service account name +func GetOtelAgentGatewayServiceAccount(objName string, ddaSpec *v2alpha1.DatadogAgentSpec) string { + saDefault := fmt.Sprintf("%s-%s", objName, DefaultOtelAgentGatewayResourceSuffix) + if ddaSpec.Override[v2alpha1.OtelAgentGatewayComponentName] != nil && ddaSpec.Override[v2alpha1.OtelAgentGatewayComponentName].ServiceAccountName != nil { + return *ddaSpec.Override[v2alpha1.OtelAgentGatewayComponentName].ServiceAccountName + } + return saDefault +} + // IsHostNetworkEnabled returns whether the pod should use the host's network namespace func IsHostNetworkEnabled(ddaSpec *v2alpha1.DatadogAgentSpec, component v2alpha1.ComponentName) bool { if ddaSpec.Override != nil { @@ -96,6 +105,11 @@ func GetLocalAgentServiceName(objName string, ddaSpec *v2alpha1.DatadogAgentSpec return fmt.Sprintf("%s-%s", objName, DefaultAgentResourceSuffix) } +// GetOTelAgentGatewayServiceName returns the name used for the OTel Agent Gateway service +func GetOTelAgentGatewayServiceName(objName string) string { + return fmt.Sprintf("%s-%s", objName, DefaultOtelAgentGatewayResourceSuffix) +} + // IsNetworkPolicyEnabled returns whether a network policy should be created and which flavor to use func IsNetworkPolicyEnabled(ddaSpec *v2alpha1.DatadogAgentSpec) (bool, v2alpha1.NetworkPolicyFlavor) { if ddaSpec.Global != nil && ddaSpec.Global.NetworkPolicy != nil && apiutils.BoolValue(ddaSpec.Global.NetworkPolicy.Create) { diff --git a/pkg/testutils/builder.go b/pkg/testutils/builder.go index cc62d20a7e..26c3ffaf72 100644 --- a/pkg/testutils/builder.go +++ b/pkg/testutils/builder.go @@ -13,7 +13,7 @@ import ( "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" apiutils "github.com/DataDog/datadog-operator/api/utils" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/defaults" - "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/otelcollector/defaultconfig" + otelcollectordefaultconfig "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/otelcollector/defaultconfig" "github.com/DataDog/datadog-operator/pkg/images" ) @@ -398,7 +398,7 @@ func (builder *DatadogAgentBuilder) WithOTelCollectorEnabled(enabled bool) *Data func (builder *DatadogAgentBuilder) WithOTelCollectorConfig() *DatadogAgentBuilder { builder.datadogAgent.Spec.Features.OtelCollector.Conf = &v2alpha1.CustomConfig{} - builder.datadogAgent.Spec.Features.OtelCollector.Conf.ConfigData = apiutils.NewStringPointer(defaultconfig.DefaultOtelCollectorConfig) + builder.datadogAgent.Spec.Features.OtelCollector.Conf.ConfigData = apiutils.NewStringPointer(otelcollectordefaultconfig.DefaultOtelCollectorConfig) return builder } @@ -468,6 +468,19 @@ func (builder *DatadogAgentBuilder) WithOTelCollectorPorts(grpcPort int32, httpP return builder } +// OtelAgentGateway +func (builder *DatadogAgentBuilder) initOtelAgentGateway() { + if builder.datadogAgent.Spec.Features.OtelAgentGateway == nil { + builder.datadogAgent.Spec.Features.OtelAgentGateway = &v2alpha1.OtelAgentGatewayFeatureConfig{} + } +} + +func (builder *DatadogAgentBuilder) WithOTelAgentGatewayEnabled(enabled bool) *DatadogAgentBuilder { + builder.initOtelAgentGateway() + builder.datadogAgent.Spec.Features.OtelAgentGateway.Enabled = apiutils.NewBoolPointer(enabled) + return builder +} + // Log Collection func (builder *DatadogAgentBuilder) initLogCollection() { if builder.datadogAgent.Spec.Features.LogCollection == nil { From 185911c223c0d9ba02f5533175f2439039a3050a Mon Sep 17 00:00:00 2001 From: Yang Song Date: Wed, 24 Dec 2025 11:51:53 -0500 Subject: [PATCH 2/4] Fix registration & resource --- .../datadogagent/component/otelagentgateway/default.go | 2 +- internal/controller/datadogagent/controller.go | 1 + .../datadogagent/global/otelcollectorgateway.go | 8 +++++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/controller/datadogagent/component/otelagentgateway/default.go b/internal/controller/datadogagent/component/otelagentgateway/default.go index 2edeb6051c..99a07114a6 100644 --- a/internal/controller/datadogagent/component/otelagentgateway/default.go +++ b/internal/controller/datadogagent/component/otelagentgateway/default.go @@ -64,7 +64,7 @@ func defaultPodSpec(dda metav1.Object) corev1.PodSpec { { Name: string(apicommon.OtelAgent), Image: images.GetLatestDdotCollectorImage(), - Command: []string{"otel-agent", "--sync-delay=30s"}, + Command: []string{"otel-agent", "--sync-delay=30s", "--config=file:/etc/datadog-agent/otel-config.yaml"}, VolumeMounts: []corev1.VolumeMount{ common.GetVolumeMountForLogs(), }, diff --git a/internal/controller/datadogagent/controller.go b/internal/controller/datadogagent/controller.go index 095d3ee55a..3e4656e631 100644 --- a/internal/controller/datadogagent/controller.go +++ b/internal/controller/datadogagent/controller.go @@ -91,6 +91,7 @@ func (r *Reconciler) initializeComponentRegistry() { // Register all components r.componentRegistry.Register(NewClusterAgentComponent(r)) r.componentRegistry.Register(NewClusterChecksRunnerComponent(r)) + r.componentRegistry.Register(NewOtelAgentGatewayComponent(r)) } // NewReconciler returns a reconciler for DatadogAgent diff --git a/internal/controller/datadogagent/global/otelcollectorgateway.go b/internal/controller/datadogagent/global/otelcollectorgateway.go index 6bf5598b42..56fe8814f6 100644 --- a/internal/controller/datadogagent/global/otelcollectorgateway.go +++ b/internal/controller/datadogagent/global/otelcollectorgateway.go @@ -7,8 +7,10 @@ package global import ( "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" 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/internal/controller/datadogagent/feature" ) @@ -27,5 +29,9 @@ func ApplyGlobalSettingsOtelAgentGateway( } func applyOtelAgentGatewayResources(manager feature.PodTemplateManagers, ddaSpec *v2alpha1.DatadogAgentSpec) { - // Add any OtelAgentGateway-specific resource configuration here + // Enable the OTel collector + manager.EnvVar().AddEnvVarToContainer(apicommon.OtelAgent, &corev1.EnvVar{ + Name: "DD_OTELCOLLECTOR_ENABLED", + Value: "true", + }) } From 779ae056e4793b50d7259db68a361d98fe995173 Mon Sep 17 00:00:00 2001 From: Yang Song Date: Mon, 12 Jan 2026 18:10:14 -0500 Subject: [PATCH 3/4] Rename to otelagentgateway --- ...rgateway.yaml => datadog-agent-with-otelagentgateway.yaml} | 0 .../datadogagent/component/otelagentgateway/default.go | 4 ++-- ..._otelcollectorgateway.go => component_otelagentgateway.go} | 0 .../global/{otelcollectorgateway.go => otelagentgateway.go} | 0 ..._otelcollectorgateway.go => component_otelagentgateway.go} | 0 pkg/constants/utils.go | 2 +- 6 files changed, 3 insertions(+), 3 deletions(-) rename examples/datadogagent/{datadog-agent-with-otelcollectorgateway.yaml => datadog-agent-with-otelagentgateway.yaml} (100%) rename internal/controller/datadogagent/{component_otelcollectorgateway.go => component_otelagentgateway.go} (100%) rename internal/controller/datadogagent/global/{otelcollectorgateway.go => otelagentgateway.go} (100%) rename internal/controller/datadogagentinternal/{component_otelcollectorgateway.go => component_otelagentgateway.go} (100%) diff --git a/examples/datadogagent/datadog-agent-with-otelcollectorgateway.yaml b/examples/datadogagent/datadog-agent-with-otelagentgateway.yaml similarity index 100% rename from examples/datadogagent/datadog-agent-with-otelcollectorgateway.yaml rename to examples/datadogagent/datadog-agent-with-otelagentgateway.yaml diff --git a/internal/controller/datadogagent/component/otelagentgateway/default.go b/internal/controller/datadogagent/component/otelagentgateway/default.go index 99a07114a6..4813a9be8f 100644 --- a/internal/controller/datadogagent/component/otelagentgateway/default.go +++ b/internal/controller/datadogagent/component/otelagentgateway/default.go @@ -30,7 +30,7 @@ func GetOtelAgentGatewayRbacResourcesName(dda metav1.Object) string { return fmt.Sprintf("%s-%s", dda.GetName(), constants.DefaultOtelAgentGatewayResourceSuffix) } -// NewDefaultOtelAgentGatewayDeployment return a new default otel-collector-gateway deployment +// NewDefaultOtelAgentGatewayDeployment return a new default otel-agent-gateway deployment func NewDefaultOtelAgentGatewayDeployment(dda metav1.Object) *appsv1.Deployment { deployment := common.NewDeployment(dda, constants.DefaultOtelAgentGatewayResourceSuffix, GetOtelAgentGatewayName(dda), common.GetAgentVersion(dda), nil) @@ -44,7 +44,7 @@ func NewDefaultOtelAgentGatewayDeployment(dda metav1.Object) *appsv1.Deployment return deployment } -// NewDefaultOtelAgentGatewayPodTemplateSpec returns a default otel-collector-gateway pod template spec +// NewDefaultOtelAgentGatewayPodTemplateSpec returns a default otel-agent-gateway pod template spec func NewDefaultOtelAgentGatewayPodTemplateSpec(dda metav1.Object) *corev1.PodTemplateSpec { template := &corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ diff --git a/internal/controller/datadogagent/component_otelcollectorgateway.go b/internal/controller/datadogagent/component_otelagentgateway.go similarity index 100% rename from internal/controller/datadogagent/component_otelcollectorgateway.go rename to internal/controller/datadogagent/component_otelagentgateway.go diff --git a/internal/controller/datadogagent/global/otelcollectorgateway.go b/internal/controller/datadogagent/global/otelagentgateway.go similarity index 100% rename from internal/controller/datadogagent/global/otelcollectorgateway.go rename to internal/controller/datadogagent/global/otelagentgateway.go diff --git a/internal/controller/datadogagentinternal/component_otelcollectorgateway.go b/internal/controller/datadogagentinternal/component_otelagentgateway.go similarity index 100% rename from internal/controller/datadogagentinternal/component_otelcollectorgateway.go rename to internal/controller/datadogagentinternal/component_otelagentgateway.go diff --git a/pkg/constants/utils.go b/pkg/constants/utils.go index d13afd1ac7..6d41e00895 100644 --- a/pkg/constants/utils.go +++ b/pkg/constants/utils.go @@ -68,7 +68,7 @@ func GetClusterChecksRunnerServiceAccount(objName string, ddaSpec *v2alpha1.Data return saDefault } -// GetOtelAgentGatewayServiceAccount return the otel-collector-gateway service account name +// GetOtelAgentGatewayServiceAccount return the otel-agent-gateway service account name func GetOtelAgentGatewayServiceAccount(objName string, ddaSpec *v2alpha1.DatadogAgentSpec) string { saDefault := fmt.Sprintf("%s-%s", objName, DefaultOtelAgentGatewayResourceSuffix) if ddaSpec.Override[v2alpha1.OtelAgentGatewayComponentName] != nil && ddaSpec.Override[v2alpha1.OtelAgentGatewayComponentName].ServiceAccountName != nil { From 15dec4d472aa893020e16e58054a7b5939bb9e28 Mon Sep 17 00:00:00 2001 From: Yang Song Date: Mon, 12 Jan 2026 18:22:21 -0500 Subject: [PATCH 4/4] Register for DDAI --- internal/controller/datadogagentinternal/controller.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/controller/datadogagentinternal/controller.go b/internal/controller/datadogagentinternal/controller.go index b2d37638a4..fc0adc67fa 100644 --- a/internal/controller/datadogagentinternal/controller.go +++ b/internal/controller/datadogagentinternal/controller.go @@ -84,6 +84,7 @@ func (r *Reconciler) initializeComponentRegistry() { // Register all components r.componentRegistry.Register(NewClusterAgentComponent(r)) r.componentRegistry.Register(NewClusterChecksRunnerComponent(r)) + r.componentRegistry.Register(NewOtelAgentGatewayComponent(r)) } // NewReconciler returns a reconciler for DatadogAgent