From c6a39865c9e669067b07254e423e907f1d04ac16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Bavelier?= Date: Mon, 9 Sep 2024 16:42:23 +0200 Subject: [PATCH 1/3] global secret backend config --- api/datadoghq/common/envvar.go | 2 + api/datadoghq/v2alpha1/datadogagent_types.go | 44 +++++++- .../v2alpha1/zz_generated.deepcopy.go | 55 +++++++++- .../v2alpha1/zz_generated.openapi.go | 103 ++++++++++++++++++ .../bases/v1/datadoghq.com_datadogagents.yaml | 46 ++++++++ docs/configuration.v2alpha1.md | 5 + .../controller/datadogagent/merger/rbac.go | 12 +- .../datadogagent/override/global.go | 78 +++++++++++++ 8 files changed, 337 insertions(+), 8 deletions(-) diff --git a/api/datadoghq/common/envvar.go b/api/datadoghq/common/envvar.go index 4ddadd4be..c351bd487 100644 --- a/api/datadoghq/common/envvar.go +++ b/api/datadoghq/common/envvar.go @@ -147,6 +147,8 @@ const ( DDSBOMHostEnabled = "DD_SBOM_HOST_ENABLED" DDSBOMHostAnalyzers = "DD_SBOM_HOST_ANALYZERS" DDSecretBackendCommand = "DD_SECRET_BACKEND_COMMAND" + DDSecretBackendArguments = "DD_SECRET_BACKEND_ARGUMENTS" + DDSecretBackendTimeout = "DD_SECRET_BACKEND_TIMEOUT" DDSite = "DD_SITE" DDSystemProbeAgentEnabled = "DD_SYSTEM_PROBE_ENABLED" DDSystemProbeBPFDebugEnabled = DDSystemProbeEnvPrefix + "BPF_DEBUG" diff --git a/api/datadoghq/v2alpha1/datadogagent_types.go b/api/datadoghq/v2alpha1/datadogagent_types.go index fcf2b3c2d..974c6f70c 100644 --- a/api/datadoghq/v2alpha1/datadogagent_types.go +++ b/api/datadoghq/v2alpha1/datadogagent_types.go @@ -1072,6 +1072,10 @@ type GlobalConfig struct { // FIPS contains configuration used to customize the FIPS proxy sidecar. FIPS *FIPSConfig `json:"fips,omitempty"` + + // Configure the secret backend feature https://docs.datadoghq.com/agent/guide/secrets-management + // See also: https://github.com/DataDog/datadog-operator/blob/main/docs/secret_management.md + SecretBackend *SecretBackendConfig `json:"secretBackend,omitempty"` } // DatadogCredentials is a generic structure that holds credentials to access Datadog. @@ -1098,13 +1102,47 @@ type DatadogCredentials struct { AppSecret *commonv1.SecretConfig `json:"appSecret,omitempty"` } +// SecretBackendRolesConfig provides configuration of the secrets Datadog agents can read for the SecretBackend feature +// +k8s:openapi-gen=true +type SecretBackendRolesConfig struct { + // Namespace defines the namespace in which the secrets reside. + // +required + Namespace *string `json:"namespace,omitempty"` + + // Secrets defines the list of secrets for which a role should be created. + // +required + // +listType=set + Secrets []string `json:"secrets,omitempty"` +} + // SecretBackendConfig provides configuration for the secret backend. +// +k8s:openapi-gen=true type SecretBackendConfig struct { - // Command defines the secret backend command to use + // The secret backend command to use. Datadog provides a pre-defined binary `/readsecret_multiple_providers.sh`. + // Read more about `/readsecret_multiple_providers.sh` at https://docs.datadoghq.com/agent/configuration/secrets-management/?tab=linux#script-for-reading-from-multiple-secret-providers. Command *string `json:"command,omitempty"` - // Args defines the list of arguments to pass to the command - Args []string `json:"args,omitempty"` + // List of arguments to pass to the command (space-separated strings). + // +optional + Args *string `json:"args,omitempty"` + + // The command timeout in seconds. + // Default: `30`. + // +optional + Timeout *int32 `json:"timeout,omitempty"` + + // Whether to create a global permission allowing Datadog agents to read all Kubernetes secrets. + // Default: `false`. + // +optional + EnableGlobalPermissions *bool `json:"enableGlobalPermissions,omitempty"` + + // Roles for Datadog to read the specified secrets, replacing `enableGlobalPermissions`. + // They are defined as a list of namespace/secrets. + // Each defined namespace needs to be present in the DatadogAgent controller using `WATCH_NAMESPACE` or `DD_AGENT_WATCH_NAMESPACE`. + // See also: https://github.com/DataDog/datadog-operator/blob/main/docs/secret_management.md#how-to-deploy-the-agent-components-using-the-secret-backend-feature-with-datadogagent. + // +optional + // +listType=atomic + Roles []*SecretBackendRolesConfig `json:"roles,omitempty"` } // NetworkPolicyFlavor specifies which flavor of Network Policy to use. diff --git a/api/datadoghq/v2alpha1/zz_generated.deepcopy.go b/api/datadoghq/v2alpha1/zz_generated.deepcopy.go index 11322f795..fb1c4e7b8 100644 --- a/api/datadoghq/v2alpha1/zz_generated.deepcopy.go +++ b/api/datadoghq/v2alpha1/zz_generated.deepcopy.go @@ -1460,6 +1460,11 @@ func (in *GlobalConfig) DeepCopyInto(out *GlobalConfig) { *out = new(FIPSConfig) (*in).DeepCopyInto(*out) } + if in.SecretBackend != nil { + in, out := &in.SecretBackend, &out.SecretBackend + *out = new(SecretBackendConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GlobalConfig. @@ -2217,8 +2222,29 @@ func (in *SecretBackendConfig) DeepCopyInto(out *SecretBackendConfig) { } if in.Args != nil { in, out := &in.Args, &out.Args - *out = make([]string, len(*in)) - copy(*out, *in) + *out = new(string) + **out = **in + } + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(int32) + **out = **in + } + if in.EnableGlobalPermissions != nil { + in, out := &in.EnableGlobalPermissions, &out.EnableGlobalPermissions + *out = new(bool) + **out = **in + } + if in.Roles != nil { + in, out := &in.Roles, &out.Roles + *out = make([]*SecretBackendRolesConfig, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(SecretBackendRolesConfig) + (*in).DeepCopyInto(*out) + } + } } } @@ -2232,6 +2258,31 @@ func (in *SecretBackendConfig) DeepCopy() *SecretBackendConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretBackendRolesConfig) DeepCopyInto(out *SecretBackendRolesConfig) { + *out = *in + if in.Namespace != nil { + in, out := &in.Namespace, &out.Namespace + *out = new(string) + **out = **in + } + if in.Secrets != nil { + in, out := &in.Secrets, &out.Secrets + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretBackendRolesConfig. +func (in *SecretBackendRolesConfig) DeepCopy() *SecretBackendRolesConfig { + if in == nil { + return nil + } + out := new(SecretBackendRolesConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Selector) DeepCopyInto(out *Selector) { *out = *in diff --git a/api/datadoghq/v2alpha1/zz_generated.openapi.go b/api/datadoghq/v2alpha1/zz_generated.openapi.go index f0bbe5ecd..64cffef90 100644 --- a/api/datadoghq/v2alpha1/zz_generated.openapi.go +++ b/api/datadoghq/v2alpha1/zz_generated.openapi.go @@ -43,6 +43,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "./api/datadoghq/v2alpha1.PrometheusScrapeFeatureConfig": schema__api_datadoghq_v2alpha1_PrometheusScrapeFeatureConfig(ref), "./api/datadoghq/v2alpha1.RemoteConfigConfiguration": schema__api_datadoghq_v2alpha1_RemoteConfigConfiguration(ref), "./api/datadoghq/v2alpha1.SeccompConfig": schema__api_datadoghq_v2alpha1_SeccompConfig(ref), + "./api/datadoghq/v2alpha1.SecretBackendConfig": schema__api_datadoghq_v2alpha1_SecretBackendConfig(ref), + "./api/datadoghq/v2alpha1.SecretBackendRolesConfig": schema__api_datadoghq_v2alpha1_SecretBackendRolesConfig(ref), "./api/datadoghq/v2alpha1.UnixDomainSocketConfig": schema__api_datadoghq_v2alpha1_UnixDomainSocketConfig(ref), } } @@ -1233,6 +1235,107 @@ func schema__api_datadoghq_v2alpha1_SeccompConfig(ref common.ReferenceCallback) } } +func schema__api_datadoghq_v2alpha1_SecretBackendConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SecretBackendConfig provides configuration for the secret backend.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "command": { + SchemaProps: spec.SchemaProps{ + Description: "The secret backend command to use. Datadog provides a pre-defined binary `/readsecret_multiple_providers.sh`. Read more about `/readsecret_multiple_providers.sh` at https://docs.datadoghq.com/agent/configuration/secrets-management/?tab=linux#script-for-reading-from-multiple-secret-providers.", + Type: []string{"string"}, + Format: "", + }, + }, + "args": { + SchemaProps: spec.SchemaProps{ + Description: "List of arguments to pass to the command (space-separated strings).", + Type: []string{"string"}, + Format: "", + }, + }, + "timeout": { + SchemaProps: spec.SchemaProps{ + Description: "The command timeout in seconds. Default: `30`.", + Type: []string{"integer"}, + Format: "int32", + }, + }, + "enableGlobalPermissions": { + SchemaProps: spec.SchemaProps{ + Description: "Whether to create a global permission allowing Datadog agents to read all Kubernetes secrets. Default: `false`.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "roles": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Roles for Datadog to read the specified secrets, replacing `enableGlobalPermissions`. They are defined as a list of namespace/secrets. Each defined namespace needs to be present in the DatadogAgent controller using `WATCH_NAMESPACE` or `DD_AGENT_WATCH_NAMESPACE`. See also: https://github.com/DataDog/datadog-operator/blob/main/docs/secret_management.md#how-to-deploy-the-agent-components-using-the-secret-backend-feature-with-datadogagent.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("./api/datadoghq/v2alpha1.SecretBackendRolesConfig"), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + "./api/datadoghq/v2alpha1.SecretBackendRolesConfig"}, + } +} + +func schema__api_datadoghq_v2alpha1_SecretBackendRolesConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SecretBackendRolesConfig provides configuration of the secrets Datadog agents can read for the SecretBackend feature", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "namespace": { + SchemaProps: spec.SchemaProps{ + Description: "Namespace defines the namespace in which the secrets reside.", + Type: []string{"string"}, + Format: "", + }, + }, + "secrets": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "set", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Secrets defines the list of secrets for which a role should be created.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, + } +} + func schema__api_datadoghq_v2alpha1_UnixDomainSocketConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/config/crd/bases/v1/datadoghq.com_datadogagents.yaml b/config/crd/bases/v1/datadoghq.com_datadogagents.yaml index 596447a82..c3b4fe624 100644 --- a/config/crd/bases/v1/datadoghq.com_datadogagents.yaml +++ b/config/crd/bases/v1/datadoghq.com_datadogagents.yaml @@ -1947,6 +1947,52 @@ spec: Use 'docker.io/datadog' for DockerHub. Default: 'gcr.io/datadoghq' type: string + secretBackend: + description: |- + Configure the secret backend feature https://docs.datadoghq.com/agent/guide/secrets-management + See also: https://github.com/DataDog/datadog-operator/blob/main/docs/secret_management.md + properties: + args: + description: List of arguments to pass to the command (space-separated strings). + type: string + command: + description: |- + The secret backend command to use. Datadog provides a pre-defined binary `/readsecret_multiple_providers.sh`. + Read more about `/readsecret_multiple_providers.sh` at https://docs.datadoghq.com/agent/configuration/secrets-management/?tab=linux#script-for-reading-from-multiple-secret-providers. + type: string + enableGlobalPermissions: + description: |- + Whether to create a global permission allowing Datadog agents to read all Kubernetes secrets. + Default: `false`. + type: boolean + roles: + description: |- + Roles for Datadog to read the specified secrets, replacing `enableGlobalPermissions`. + They are defined as a list of namespace/secrets. + Each defined namespace needs to be present in the DatadogAgent controller using `WATCH_NAMESPACE` or `DD_AGENT_WATCH_NAMESPACE`. + See also: https://github.com/DataDog/datadog-operator/blob/main/docs/secret_management.md#how-to-deploy-the-agent-components-using-the-secret-backend-feature-with-datadogagent. + items: + description: SecretBackendRolesConfig provides configuration of the secrets Datadog agents can read for the SecretBackend feature + properties: + namespace: + description: Namespace defines the namespace in which the secrets reside. + type: string + secrets: + description: Secrets defines the list of secrets for which a role should be created. + items: + type: string + type: array + x-kubernetes-list-type: set + type: object + type: array + x-kubernetes-list-type: atomic + timeout: + description: |- + The command timeout in seconds. + Default: `30`. + format: int32 + type: integer + type: object site: description: |- Site is the Datadog intake site Agent data are sent to. diff --git a/docs/configuration.v2alpha1.md b/docs/configuration.v2alpha1.md index 81f97a5be..56a6de27e 100644 --- a/docs/configuration.v2alpha1.md +++ b/docs/configuration.v2alpha1.md @@ -222,6 +222,11 @@ spec: | global.podAnnotationsAsTags | Provide a mapping of Kubernetes Annotations to Datadog Tags. : | | global.podLabelsAsTags | Provide a mapping of Kubernetes Labels to Datadog Tags. : | | global.registry | Registry is the image registry to use for all Agent images. Use 'public.ecr.aws/datadog' for AWS ECR. Use 'docker.io/datadog' for DockerHub. Default: 'gcr.io/datadoghq' | +| global.secretBackend.args | List of arguments to pass to the command (space-separated strings). | +| global.secretBackend.command | The secret backend command to use. Datadog provides a pre-defined binary `/readsecret_multiple_providers.sh`. Read more about `/readsecret_multiple_providers.sh` at https://docs.datadoghq.com/agent/configuration/secrets-management/?tab=linux#script-for-reading-from-multiple-secret-providers. | +| global.secretBackend.enableGlobalPermissions | Whether to create a global permission allowing Datadog agents to read all Kubernetes secrets. Default: `false`. | +| global.secretBackend.roles | Roles for Datadog to read the specified secrets, replacing `enableGlobalPermissions`. They are defined as a list of namespace/secrets. Each defined namespace needs to be present in the DatadogAgent controller using `WATCH_NAMESPACE` or `DD_AGENT_WATCH_NAMESPACE`. See also: https://github.com/DataDog/datadog-operator/blob/main/docs/secret_management.md#how-to-deploy-the-agent-components-using-the-secret-backend-feature-with-datadogagent. | +| global.secretBackend.timeout | The command timeout in seconds. Default: `30`. | | global.site | Site is the Datadog intake site Agent data are sent to. Set to 'datadoghq.com' to send data to the US1 site (default). Set to 'datadoghq.eu' to send data to the EU site. Set to 'us3.datadoghq.com' to send data to the US3 site. Set to 'us5.datadoghq.com' to send data to the US5 site. Set to 'ddog-gov.com' to send data to the US1-FED site. Set to 'ap1.datadoghq.com' to send data to the AP1 site. Default: 'datadoghq.com' | | global.tags | Tags contains a list of tags to attach to every metric, event and service check collected. Learn more about tagging: https://docs.datadoghq.com/tagging/ | | override | Override the default configurations of the agents | diff --git a/internal/controller/datadogagent/merger/rbac.go b/internal/controller/datadogagent/merger/rbac.go index 02732783b..a3a4d2761 100644 --- a/internal/controller/datadogagent/merger/rbac.go +++ b/internal/controller/datadogagent/merger/rbac.go @@ -21,7 +21,7 @@ import ( type RBACManager interface { AddServiceAccount(namespace string, name string) error AddServiceAccountByComponent(namespace, name, component string) error - AddPolicyRules(namespace string, roleName string, saName string, policies []rbacv1.PolicyRule) error + AddPolicyRules(namespace string, roleName string, saName string, policies []rbacv1.PolicyRule, saNamespace ...string) error AddPolicyRulesByComponent(namespace string, roleName string, saName string, policies []rbacv1.PolicyRule, component string) error AddRoleBinding(roleNamespace, roleName, saNamespace, saName string, roleRef rbacv1.RoleRef) error AddClusterPolicyRules(namespace string, roleName string, saName string, policies []rbacv1.PolicyRule) error @@ -87,7 +87,7 @@ func (m *rbacManagerImpl) DeleteServiceAccountByComponent(component, namespace s } // AddPolicyRules is used to add PolicyRules to a Role. It also creates the RoleBinding. -func (m *rbacManagerImpl) AddPolicyRules(namespace string, roleName string, saName string, policies []rbacv1.PolicyRule) error { +func (m *rbacManagerImpl) AddPolicyRules(namespace string, roleName string, saName string, policies []rbacv1.PolicyRule, saNamespace ...string) error { obj, _ := m.store.GetOrCreate(kubernetes.RolesKind, namespace, roleName) role, ok := obj.(*rbacv1.Role) if !ok { @@ -106,7 +106,13 @@ func (m *rbacManagerImpl) AddPolicyRules(namespace string, roleName string, saNa Name: roleName, } - return m.AddRoleBinding(namespace, roleName, namespace, saName, roleRef) + // If saNamespace is not provided, defaults to using role namespace. + targetSaNamespace := namespace + if len(saNamespace) > 0 { + targetSaNamespace = saNamespace[0] + } + + return m.AddRoleBinding(namespace, roleName, targetSaNamespace, saName, roleRef) } // AddPolicyRulesByComponent is used to add PolicyRules to a Role, create a RoleBinding, and associate them with a component diff --git a/internal/controller/datadogagent/override/global.go b/internal/controller/datadogagent/override/global.go index b99f74ce1..c0563978f 100644 --- a/internal/controller/datadogagent/override/global.go +++ b/internal/controller/datadogagent/override/global.go @@ -10,6 +10,8 @@ import ( "fmt" "path/filepath" + "strconv" + apicommon "github.com/DataDog/datadog-operator/api/datadoghq/common" apicommonv1 "github.com/DataDog/datadog-operator/api/datadoghq/common/v1" "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1" @@ -18,6 +20,8 @@ import ( "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/object/volume" "github.com/DataDog/datadog-operator/pkg/defaulting" + "github.com/DataDog/datadog-operator/pkg/kubernetes/rbac" + rbacv1 "k8s.io/api/rbac/v1" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" @@ -307,6 +311,80 @@ func applyGlobalSettings(logger logr.Logger, manager feature.PodTemplateManagers } } + // Apply SecretBackend config + if config.SecretBackend != nil { + // Set secret backend command + manager.EnvVar().AddEnvVar(&corev1.EnvVar{ + Name: apicommon.DDSecretBackendCommand, + Value: apiutils.StringValue(config.SecretBackend.Command), + }) + + // Set secret backend arguments + manager.EnvVar().AddEnvVar(&corev1.EnvVar{ + Name: apicommon.DDSecretBackendArguments, + Value: apiutils.StringValue(config.SecretBackend.Args), + }) + + // Set secret backend timeout + if config.SecretBackend.Timeout != nil { + manager.EnvVar().AddEnvVar(&corev1.EnvVar{ + Name: apicommon.DDSecretBackendTimeout, + Value: strconv.FormatInt(int64(*config.SecretBackend.Timeout), 10), + }) + } + + var componentSaName string + switch componentName { + case v2alpha1.ClusterAgentComponentName: + componentSaName = v2alpha1.GetClusterAgentServiceAccount(dda) + case v2alpha1.NodeAgentComponentName: + componentSaName = v2alpha1.GetAgentServiceAccount(dda) + case v2alpha1.ClusterChecksRunnerComponentName: + componentSaName = v2alpha1.GetClusterChecksRunnerServiceAccount(dda) + } + + agentName := dda.GetName() + agentNs := dda.GetNamespace() + rbacSuffix := "secret-backend" + + // Set global RBAC config (only if specific roles are not defined) + if apiutils.BoolValue(config.SecretBackend.EnableGlobalPermissions) && config.SecretBackend.Roles == nil { + + var secretBackendGlobalRBACPolicyRules = []rbacv1.PolicyRule{ + { + APIGroups: []string{rbac.CoreAPIGroup}, + Resources: []string{rbac.SecretsResource}, + Verbs: []string{rbac.GetVerb}, + }, + } + + roleName := fmt.Sprintf("%s-%s-%s", agentNs, agentName, rbacSuffix) + + if err := resourcesManager.RBACManager().AddClusterPolicyRules(agentNs, roleName, componentSaName, secretBackendGlobalRBACPolicyRules); err != nil { + logger.Error(err, "Error adding cluster-wide secrets RBAC policy") + } + } + + // Set specific roles for the secret backend + if config.SecretBackend.Roles != nil { + for _, role := range config.SecretBackend.Roles { + secretNs := apiutils.StringValue(role.Namespace) + roleName := fmt.Sprintf("%s-%s-%s", secretNs, agentName, rbacSuffix) + policyRule := []rbacv1.PolicyRule{ + { + APIGroups: []string{rbac.CoreAPIGroup}, + Resources: []string{rbac.SecretsResource}, + ResourceNames: role.Secrets, + Verbs: []string{rbac.GetVerb}, + }, + } + if err := resourcesManager.RBACManager().AddPolicyRules(secretNs, roleName, componentSaName, policyRule, agentNs); err != nil { + logger.Error(err, "Error adding secrets RBAC policy") + } + } + } + } + // Apply FIPS config if config.FIPS != nil && apiutils.BoolValue(config.FIPS.Enabled) { applyFIPSConfig(logger, manager, dda, resourcesManager) From 42270bafb259d2c0916e7a9dab3c2c400bac56ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Bavelier?= Date: Mon, 9 Sep 2024 16:43:34 +0200 Subject: [PATCH 2/3] tests --- api/datadoghq/v2alpha1/test/builder.go | 28 +++ .../datadogagent/override/global_test.go | 204 +++++++++++++++++- 2 files changed, 229 insertions(+), 3 deletions(-) diff --git a/api/datadoghq/v2alpha1/test/builder.go b/api/datadoghq/v2alpha1/test/builder.go index 8fc819d02..a7c5e7968 100644 --- a/api/datadoghq/v2alpha1/test/builder.go +++ b/api/datadoghq/v2alpha1/test/builder.go @@ -778,6 +778,34 @@ func (builder *DatadogAgentBuilder) WithRegistry(registry string) *DatadogAgentB return builder } +// Global SecretBackend + +func (builder *DatadogAgentBuilder) WithGlobalSecretBackendGlobalPerms(command string, args string, timeout int32) *DatadogAgentBuilder { + builder.datadogAgent.Spec.Global.SecretBackend = &v2alpha1.SecretBackendConfig{ + Command: apiutils.NewStringPointer(command), + Args: apiutils.NewStringPointer(args), + Timeout: apiutils.NewInt32Pointer(timeout), + EnableGlobalPermissions: apiutils.NewBoolPointer(true), + } + return builder +} + +func (builder *DatadogAgentBuilder) WithGlobalSecretBackendSpecificRoles(command string, args string, timeout int32, secretNs string, secretNames []string) *DatadogAgentBuilder { + builder.datadogAgent.Spec.Global.SecretBackend = &v2alpha1.SecretBackendConfig{ + Command: apiutils.NewStringPointer(command), + Args: apiutils.NewStringPointer(args), + Timeout: apiutils.NewInt32Pointer(timeout), + EnableGlobalPermissions: apiutils.NewBoolPointer(false), + Roles: []*v2alpha1.SecretBackendRolesConfig{ + { + Namespace: apiutils.NewStringPointer(secretNs), + Secrets: secretNames, + }, + }, + } + return builder +} + // Override func (builder *DatadogAgentBuilder) WithComponentOverride(componentName v2alpha1.ComponentName, override v2alpha1.DatadogAgentComponentOverride) *DatadogAgentBuilder { diff --git a/internal/controller/datadogagent/override/global_test.go b/internal/controller/datadogagent/override/global_test.go index 9f5cec553..659570d12 100644 --- a/internal/controller/datadogagent/override/global_test.go +++ b/internal/controller/datadogagent/override/global_test.go @@ -6,8 +6,12 @@ package override import ( + "fmt" "testing" + "github.com/DataDog/datadog-operator/pkg/kubernetes" + "github.com/DataDog/datadog-operator/pkg/kubernetes/rbac" + apicommon "github.com/DataDog/datadog-operator/api/datadoghq/common" apicommonv1 "github.com/DataDog/datadog-operator/api/datadoghq/common/v1" @@ -21,16 +25,25 @@ import ( "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature" "github.com/DataDog/datadog-operator/internal/controller/datadogagent/feature/fake" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" ) const ( - hostCAPath = "/host/ca/path/ca.crt" - agentCAPath = "/agent/ca/path/ca.crt" - dockerSocketPath = "/docker/socket/path/docker.sock" + hostCAPath = "/host/ca/path/ca.crt" + agentCAPath = "/agent/ca/path/ca.crt" + dockerSocketPath = "/docker/socket/path/docker.sock" + secretBackendCommand = "foo.sh" + secretBackendArgs = "bar baz" + secretBackendTimeout = 60 + ddaName = "datadog" + ddaNamespace = "system" + secretNamespace = "postgres" ) +var secretNames = []string{"db-username", "db-password"} + func TestNodeAgentComponenGlobalSettings(t *testing.T) { logger := logf.Log.WithName("TestRequiredComponents") @@ -51,6 +64,7 @@ func TestNodeAgentComponenGlobalSettings(t *testing.T) { wantVolumes []*corev1.Volume wantEnvVars []*corev1.EnvVar want func(t testing.TB, mgrInterface feature.PodTemplateManagers, expectedEnvVars []*corev1.EnvVar, expectedVolumes []*corev1.Volume, expectedVolumeMounts []*corev1.VolumeMount) + wantDependency func(t testing.TB, resourcesManager feature.ResourceManagers) }{ { name: "Kubelet volume configured", @@ -116,6 +130,64 @@ func TestNodeAgentComponenGlobalSettings(t *testing.T) { wantVolumes: emptyVolumes, want: assertAll, }, + { + name: "Secret backend - global permissions", + singleContainerStrategyEnabled: false, + dda: addNameNamespaceToDDA( + ddaName, + ddaNamespace, + v2alpha1test.NewDatadogAgentBuilder(). + WithGlobalSecretBackendGlobalPerms(secretBackendCommand, secretBackendArgs, secretBackendTimeout). + BuildWithDefaults(), + ), + wantEnvVars: getExpectedEnvVars([]*corev1.EnvVar{ + { + Name: apicommon.DDSecretBackendCommand, + Value: secretBackendCommand, + }, + { + Name: apicommon.DDSecretBackendArguments, + Value: secretBackendArgs, + }, + { + Name: apicommon.DDSecretBackendTimeout, + Value: "60", + }, + }...), + wantVolumeMounts: emptyVolumeMounts, + wantVolumes: emptyVolumes, + want: assertAll, + wantDependency: assertSecretBackendGlobalPerms, + }, + { + name: "Secret backend - specific secret permissions", + singleContainerStrategyEnabled: false, + dda: addNameNamespaceToDDA( + ddaName, + ddaNamespace, + v2alpha1test.NewDatadogAgentBuilder(). + WithGlobalSecretBackendSpecificRoles(secretBackendCommand, secretBackendArgs, secretBackendTimeout, secretNamespace, secretNames). + BuildWithDefaults(), + ), + wantEnvVars: getExpectedEnvVars([]*corev1.EnvVar{ + { + Name: apicommon.DDSecretBackendCommand, + Value: secretBackendCommand, + }, + { + Name: apicommon.DDSecretBackendArguments, + Value: secretBackendArgs, + }, + { + Name: apicommon.DDSecretBackendTimeout, + Value: "60", + }, + }...), + wantVolumeMounts: emptyVolumeMounts, + wantVolumes: emptyVolumes, + want: assertAll, + wantDependency: assertSecretBackendSpecificPerms, + }, } for _, tt := range tests { @@ -127,6 +199,10 @@ func TestNodeAgentComponenGlobalSettings(t *testing.T) { ApplyGlobalSettingsNodeAgent(logger, podTemplateManager, tt.dda, resourcesManager, tt.singleContainerStrategyEnabled) tt.want(t, podTemplateManager, tt.wantEnvVars, tt.wantVolumes, tt.wantVolumeMounts) + // Assert dependencies if and only if a dependency is expected + if tt.wantDependency != nil { + tt.wantDependency(t, resourcesManager) + } }) } } @@ -213,3 +289,125 @@ func getExpectedVolumeMounts() []*corev1.VolumeMount { }, } } + +func addNameNamespaceToDDA(name string, namespace string, dda *v2alpha1.DatadogAgent) *v2alpha1.DatadogAgent { + dda.Name = name + dda.Namespace = namespace + return dda +} + +func assertSecretBackendGlobalPerms(t testing.TB, resourcesManager feature.ResourceManagers) { + store := resourcesManager.Store() + // ClusterRole and ClusterRoleBinding use the same name + expectedName := fmt.Sprintf("%s-%s-%s", ddaNamespace, ddaName, "secret-backend") + expectedPolicyRules := []rbacv1.PolicyRule{ + { + APIGroups: []string{rbac.CoreAPIGroup}, + Resources: []string{rbac.SecretsResource}, + Verbs: []string{rbac.GetVerb}, + }, + } + crObj, found := store.Get(kubernetes.ClusterRolesKind, "", expectedName) + if !found { + t.Error("Should have created ClusterRole") + } else { + cr := crObj.(*rbacv1.ClusterRole) + assert.True( + t, + apiutils.IsEqualStruct(cr.Rules, expectedPolicyRules), + "ClusterRole Policy Rules \ndiff = %s", cmp.Diff(cr.Rules, expectedPolicyRules), + ) + } + + expectedRoleRef := rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: rbac.ClusterRoleKind, + Name: expectedName, + } + + expectedSubject := []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: ddaName + "-" + apicommon.DefaultAgentResourceSuffix, + Namespace: ddaNamespace, + }, + } + + crbObj, found := store.Get(kubernetes.ClusterRoleBindingKind, "", expectedName) + if !found { + t.Error("Should have created ClusterRoleBinding") + } else { + crb := crbObj.(*rbacv1.ClusterRoleBinding) + // Validate ClusterRoleBinding roleRef name + assert.True( + t, + apiutils.IsEqualStruct(crb.RoleRef, expectedRoleRef), + "ClusterRoleBinding Role Ref \ndiff = %s", cmp.Diff(crb.RoleRef, expectedRoleRef), + ) + // Validate ClusterRoleBinding subject + assert.True( + t, + apiutils.IsEqualStruct(crb.Subjects, expectedSubject), + "ClusterRoleBinding Subject \ndiff = %s", cmp.Diff(crb.Subjects, expectedSubject), + ) + } +} + +func assertSecretBackendSpecificPerms(t testing.TB, resourcesManager feature.ResourceManagers) { + store := resourcesManager.Store() + + // Role and RoleBinding use the same name + expectedName := fmt.Sprintf("%s-%s-%s", secretNamespace, ddaName, "secret-backend") + expectedPolicyRules := []rbacv1.PolicyRule{ + { + APIGroups: []string{rbac.CoreAPIGroup}, + Resources: []string{rbac.SecretsResource}, + ResourceNames: secretNames, + Verbs: []string{rbac.GetVerb}, + }, + } + rObj, found := store.Get(kubernetes.RolesKind, secretNamespace, expectedName) + if !found { + t.Error("Should have created Role") + } else { + r := rObj.(*rbacv1.Role) + assert.True( + t, + apiutils.IsEqualStruct(r.Rules, expectedPolicyRules), + "Role Policy Rules \ndiff = %s", cmp.Diff(r.Rules, expectedPolicyRules), + ) + } + + expectedRoleRef := rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: rbac.RoleKind, + Name: expectedName, + } + + expectedSubject := []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: ddaName + "-" + apicommon.DefaultAgentResourceSuffix, + Namespace: ddaNamespace, + }, + } + + rbObj, found := store.Get(kubernetes.RoleBindingKind, secretNamespace, expectedName) + if !found { + t.Error("Should have created RoleBinding") + } else { + rb := rbObj.(*rbacv1.RoleBinding) + // Validate RoleBinding roleRef name + assert.True( + t, + apiutils.IsEqualStruct(rb.RoleRef, expectedRoleRef), + "RoleBinding Role Ref \ndiff = %s", cmp.Diff(rb.RoleRef, expectedRoleRef), + ) + // Validate RoleBinding subject + assert.True( + t, + apiutils.IsEqualStruct(rb.Subjects, expectedSubject), + "RoleBinding Subject \ndiff = %s", cmp.Diff(rb.Subjects, expectedSubject), + ) + } +} From b981128226ff541f089306c1f7ab3422726c4ec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Bavelier?= Date: Thu, 3 Oct 2024 09:20:59 +0200 Subject: [PATCH 3/3] fix DefaultAgentResourceSuffix that changed from apicommon to v2alpha1 --- internal/controller/datadogagent/override/global_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/controller/datadogagent/override/global_test.go b/internal/controller/datadogagent/override/global_test.go index cd4371a66..cc153baa3 100644 --- a/internal/controller/datadogagent/override/global_test.go +++ b/internal/controller/datadogagent/override/global_test.go @@ -355,7 +355,7 @@ func assertSecretBackendGlobalPerms(t testing.TB, resourcesManager feature.Resou expectedSubject := []rbacv1.Subject{ { Kind: "ServiceAccount", - Name: ddaName + "-" + apicommon.DefaultAgentResourceSuffix, + Name: ddaName + "-" + v2alpha1.DefaultAgentResourceSuffix, Namespace: ddaNamespace, }, } @@ -414,7 +414,7 @@ func assertSecretBackendSpecificPerms(t testing.TB, resourcesManager feature.Res expectedSubject := []rbacv1.Subject{ { Kind: "ServiceAccount", - Name: ddaName + "-" + apicommon.DefaultAgentResourceSuffix, + Name: ddaName + "-" + v2alpha1.DefaultAgentResourceSuffix, Namespace: ddaNamespace, }, }