diff --git a/config/crds/v1/all-crds.yaml b/config/crds/v1/all-crds.yaml index a2b17d7416e..0bba97e056d 100644 --- a/config/crds/v1/all-crds.yaml +++ b/config/crds/v1/all-crds.yaml @@ -10186,6 +10186,10 @@ spec: (/_cluster/settings) type: object x-kubernetes-preserve-unknown-fields: true + config: + description: Config holds the settings that go into elasticsearch.yml. + type: object + x-kubernetes-preserve-unknown-fields: true indexLifecyclePolicies: description: IndexLifecyclePolicies holds the Index Lifecycle policies settings (/_ilm/policy) @@ -10212,6 +10216,24 @@ spec: (/_ingest/pipeline) type: object x-kubernetes-preserve-unknown-fields: true + secretMounts: + description: SecretMounts are additional Secrets that need to + be mounted into the Elasticsearch pods. + items: + description: SecretMount contains information about additional + secrets to be mounted to the elasticsearch pods + properties: + mountPath: + description: MountPath denotes the path to which the secret + should be mounted to inside the elasticsearch pod + type: string + secretName: + description: SecretName denotes the name of the secret that + needs to be mounted to the elasticsearch pod + type: string + type: object + type: array + x-kubernetes-preserve-unknown-fields: true securityRoleMappings: description: SecurityRoleMappings holds the Role Mappings settings (/_security/role_mapping) @@ -10228,6 +10250,31 @@ spec: type: object x-kubernetes-preserve-unknown-fields: true type: object + kibana: + properties: + config: + description: Config holds the settings that go into kibana.yml. + type: object + x-kubernetes-preserve-unknown-fields: true + secretMounts: + description: SecretMounts are additional secrets that need to + be mounted into the Kibana pods. + items: + description: SecretMount contains information about additional + secrets to be mounted to the elasticsearch pods + properties: + mountPath: + description: MountPath denotes the path to which the secret + should be mounted to inside the elasticsearch pod + type: string + secretName: + description: SecretName denotes the name of the secret that + needs to be mounted to the elasticsearch pod + type: string + type: object + type: array + x-kubernetes-preserve-unknown-fields: true + type: object resourceSelector: description: A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty diff --git a/config/crds/v1/bases/stackconfigpolicy.k8s.elastic.co_stackconfigpolicies.yaml b/config/crds/v1/bases/stackconfigpolicy.k8s.elastic.co_stackconfigpolicies.yaml index 50fbac5131d..0e1b0c7cca8 100644 --- a/config/crds/v1/bases/stackconfigpolicy.k8s.elastic.co_stackconfigpolicies.yaml +++ b/config/crds/v1/bases/stackconfigpolicy.k8s.elastic.co_stackconfigpolicies.yaml @@ -56,6 +56,10 @@ spec: (/_cluster/settings) type: object x-kubernetes-preserve-unknown-fields: true + config: + description: Config holds the settings that go into elasticsearch.yml. + type: object + x-kubernetes-preserve-unknown-fields: true indexLifecyclePolicies: description: IndexLifecyclePolicies holds the Index Lifecycle policies settings (/_ilm/policy) @@ -82,6 +86,24 @@ spec: (/_ingest/pipeline) type: object x-kubernetes-preserve-unknown-fields: true + secretMounts: + description: SecretMounts are additional Secrets that need to + be mounted into the Elasticsearch pods. + items: + description: SecretMount contains information about additional + secrets to be mounted to the elasticsearch pods + properties: + mountPath: + description: MountPath denotes the path to which the secret + should be mounted to inside the elasticsearch pod + type: string + secretName: + description: SecretName denotes the name of the secret that + needs to be mounted to the elasticsearch pod + type: string + type: object + type: array + x-kubernetes-preserve-unknown-fields: true securityRoleMappings: description: SecurityRoleMappings holds the Role Mappings settings (/_security/role_mapping) @@ -98,6 +120,31 @@ spec: type: object x-kubernetes-preserve-unknown-fields: true type: object + kibana: + properties: + config: + description: Config holds the settings that go into kibana.yml. + type: object + x-kubernetes-preserve-unknown-fields: true + secretMounts: + description: SecretMounts are additional secrets that need to + be mounted into the Kibana pods. + items: + description: SecretMount contains information about additional + secrets to be mounted to the elasticsearch pods + properties: + mountPath: + description: MountPath denotes the path to which the secret + should be mounted to inside the elasticsearch pod + type: string + secretName: + description: SecretName denotes the name of the secret that + needs to be mounted to the elasticsearch pod + type: string + type: object + type: array + x-kubernetes-preserve-unknown-fields: true + type: object resourceSelector: description: A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty diff --git a/deploy/eck-operator/charts/eck-operator-crds/templates/all-crds.yaml b/deploy/eck-operator/charts/eck-operator-crds/templates/all-crds.yaml index cf461d62d46..d8ac2d14b7b 100644 --- a/deploy/eck-operator/charts/eck-operator-crds/templates/all-crds.yaml +++ b/deploy/eck-operator/charts/eck-operator-crds/templates/all-crds.yaml @@ -10246,6 +10246,10 @@ spec: (/_cluster/settings) type: object x-kubernetes-preserve-unknown-fields: true + config: + description: Config holds the settings that go into elasticsearch.yml. + type: object + x-kubernetes-preserve-unknown-fields: true indexLifecyclePolicies: description: IndexLifecyclePolicies holds the Index Lifecycle policies settings (/_ilm/policy) @@ -10272,6 +10276,24 @@ spec: (/_ingest/pipeline) type: object x-kubernetes-preserve-unknown-fields: true + secretMounts: + description: SecretMounts are additional Secrets that need to + be mounted into the Elasticsearch pods. + items: + description: SecretMount contains information about additional + secrets to be mounted to the elasticsearch pods + properties: + mountPath: + description: MountPath denotes the path to which the secret + should be mounted to inside the elasticsearch pod + type: string + secretName: + description: SecretName denotes the name of the secret that + needs to be mounted to the elasticsearch pod + type: string + type: object + type: array + x-kubernetes-preserve-unknown-fields: true securityRoleMappings: description: SecurityRoleMappings holds the Role Mappings settings (/_security/role_mapping) @@ -10288,6 +10310,31 @@ spec: type: object x-kubernetes-preserve-unknown-fields: true type: object + kibana: + properties: + config: + description: Config holds the settings that go into kibana.yml. + type: object + x-kubernetes-preserve-unknown-fields: true + secretMounts: + description: SecretMounts are additional secrets that need to + be mounted into the Kibana pods. + items: + description: SecretMount contains information about additional + secrets to be mounted to the elasticsearch pods + properties: + mountPath: + description: MountPath denotes the path to which the secret + should be mounted to inside the elasticsearch pod + type: string + secretName: + description: SecretName denotes the name of the secret that + needs to be mounted to the elasticsearch pod + type: string + type: object + type: array + x-kubernetes-preserve-unknown-fields: true + type: object resourceSelector: description: A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty diff --git a/docs/reference/api-docs.asciidoc b/docs/reference/api-docs.asciidoc index caaf5b75127..3e15a72097f 100644 --- a/docs/reference/api-docs.asciidoc +++ b/docs/reference/api-docs.asciidoc @@ -448,6 +448,7 @@ Config represents untyped YAML configuration. - xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-enterprisesearch-v1-enterprisesearchspec[$$EnterpriseSearchSpec$$] - xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-enterprisesearch-v1beta1-enterprisesearchspec[$$EnterpriseSearchSpec$$] - xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-stackconfigpolicy-v1alpha1-indextemplates[$$IndexTemplates$$] +- xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-stackconfigpolicy-v1alpha1-kibanaconfigpolicyspec[$$KibanaConfigPolicySpec$$] - xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-kibana-v1-kibanaspec[$$KibanaSpec$$] - xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-logstash-v1alpha1-logstashspec[$$LogstashSpec$$] - xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-maps-v1alpha1-mapsspec[$$MapsSpec$$] @@ -2076,6 +2077,8 @@ Package v1alpha1 contains API schema definitions for managing StackConfigPolicy | *`indexLifecyclePolicies`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-common-v1-config[$$Config$$]__ | IndexLifecyclePolicies holds the Index Lifecycle policies settings (/_ilm/policy) | *`ingestPipelines`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-common-v1-config[$$Config$$]__ | IngestPipelines holds the Ingest Pipelines settings (/_ingest/pipeline) | *`indexTemplates`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-stackconfigpolicy-v1alpha1-indextemplates[$$IndexTemplates$$]__ | IndexTemplates holds the Index and Component Templates settings +| *`config`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-common-v1-config[$$Config$$]__ | Config holds the settings that go into elasticsearch.yml. +| *`secretMounts`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-stackconfigpolicy-v1alpha1-secretmount[$$SecretMount$$] array__ | SecretMounts are additional Secrets that need to be mounted into the Elasticsearch pods. |=== @@ -2097,6 +2100,43 @@ Package v1alpha1 contains API schema definitions for managing StackConfigPolicy |=== +[id="{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-stackconfigpolicy-v1alpha1-kibanaconfigpolicyspec"] +=== KibanaConfigPolicySpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-stackconfigpolicy-v1alpha1-stackconfigpolicyspec[$$StackConfigPolicySpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`config`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-common-v1-config[$$Config$$]__ | Config holds the settings that go into kibana.yml. +| *`secretMounts`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-stackconfigpolicy-v1alpha1-secretmount[$$SecretMount$$] array__ | SecretMounts are additional secrets that need to be mounted into the Kibana pods. +|=== + + + + +[id="{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-stackconfigpolicy-v1alpha1-secretmount"] +=== SecretMount + +SecretMount contains information about additional secrets to be mounted to the elasticsearch pods + +.Appears In: +**** +- xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-stackconfigpolicy-v1alpha1-elasticsearchconfigpolicyspec[$$ElasticsearchConfigPolicySpec$$] +- xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-stackconfigpolicy-v1alpha1-kibanaconfigpolicyspec[$$KibanaConfigPolicySpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`secretName`* __string__ | SecretName denotes the name of the secret that needs to be mounted to the elasticsearch pod +| *`mountPath`* __string__ | MountPath denotes the path to which the secret should be mounted to inside the elasticsearch pod +|=== [id="{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-stackconfigpolicy-v1alpha1-stackconfigpolicy"] @@ -2133,6 +2173,7 @@ StackConfigPolicy represents a StackConfigPolicy resource in a Kubernetes cluste | *`resourceSelector`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#labelselector-v1-meta[$$LabelSelector$$]__ | | *`secureSettings`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-common-v1-secretsource[$$SecretSource$$] array__ | | *`elasticsearch`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-stackconfigpolicy-v1alpha1-elasticsearchconfigpolicyspec[$$ElasticsearchConfigPolicySpec$$]__ | +| *`kibana`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-v2-pkg-apis-stackconfigpolicy-v1alpha1-kibanaconfigpolicyspec[$$KibanaConfigPolicySpec$$]__ | |=== diff --git a/pkg/apis/elasticsearch/v1/name.go b/pkg/apis/elasticsearch/v1/name.go index 31a0a4efb33..6d30966df60 100644 --- a/pkg/apis/elasticsearch/v1/name.go +++ b/pkg/apis/elasticsearch/v1/name.go @@ -12,6 +12,7 @@ import ( apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" utilvalidation "k8s.io/apimachinery/pkg/util/validation" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/hash" common_name "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/name" ) @@ -19,6 +20,7 @@ const ( configSecretSuffix = "config" secureSettingsSecretSuffix = "secure-settings" fileSettingsSecretSuffix = "file-settings" + policyEsConfigSecretSuffix = "policy-config" //nolint:gosec httpServiceSuffix = "http" internalHTTPServiceSuffix = "internal-http" transportServiceSuffix = "transport" @@ -174,3 +176,14 @@ func RemoteCaSecretName(esName string) string { func FileSettingsSecretName(esName string) string { return ESNamer.Suffix(esName, fileSettingsSecretSuffix) } + +func StackConfigElasticsearchConfigSecretName(esName string) string { + return ESNamer.Suffix(esName, policyEsConfigSecretSuffix) +} + +// StackConfigAdditionalSecretName returns the name of the stack config policy Secret suffixed with a hash to prevent conflicts. +// This also helps keep the secret name size to within kubernetes name limits even if the secret name created by the user is long. +func StackConfigAdditionalSecretName(esName string, secretName string) string { + secretNameHash := hash.HashObject(secretName) + return ESNamer.Suffix(esName, "scp", secretNameHash) +} diff --git a/pkg/apis/stackconfigpolicy/v1alpha1/stackconfigpolicy_types.go b/pkg/apis/stackconfigpolicy/v1alpha1/stackconfigpolicy_types.go index 460b815cecc..4f660f7c6d5 100644 --- a/pkg/apis/stackconfigpolicy/v1alpha1/stackconfigpolicy_types.go +++ b/pkg/apis/stackconfigpolicy/v1alpha1/stackconfigpolicy_types.go @@ -55,6 +55,7 @@ type StackConfigPolicySpec struct { ResourceSelector metav1.LabelSelector `json:"resourceSelector,omitempty"` SecureSettings []commonv1.SecretSource `json:"secureSettings,omitempty"` Elasticsearch ElasticsearchConfigPolicySpec `json:"elasticsearch,omitempty"` + Kibana KibanaConfigPolicySpec `json:"kibana,omitempty"` } type ElasticsearchConfigPolicySpec struct { @@ -79,6 +80,21 @@ type ElasticsearchConfigPolicySpec struct { // IndexTemplates holds the Index and Component Templates settings // +kubebuilder:pruning:PreserveUnknownFields IndexTemplates IndexTemplates `json:"indexTemplates,omitempty"` + // Config holds the settings that go into elasticsearch.yml. + // +kubebuilder:pruning:PreserveUnknownFields + Config *commonv1.Config `json:"config,omitempty"` + // SecretMounts are additional Secrets that need to be mounted into the Elasticsearch pods. + // +kubebuilder:pruning:PreserveUnknownFields + SecretMounts []SecretMount `json:"secretMounts,omitempty"` +} + +type KibanaConfigPolicySpec struct { + // Config holds the settings that go into kibana.yml. + // +kubebuilder:pruning:PreserveUnknownFields + Config *commonv1.Config `json:"config,omitempty"` + // SecretMounts are additional secrets that need to be mounted into the Kibana pods. + // +kubebuilder:pruning:PreserveUnknownFields + SecretMounts []SecretMount `json:"secretMounts,omitempty"` } type IndexTemplates struct { @@ -142,6 +158,14 @@ type PolicyStatusError struct { Message string `json:"message,omitempty"` } +// SecretMount contains information about additional secrets to be mounted to the elasticsearch pods +type SecretMount struct { + // SecretName denotes the name of the secret that needs to be mounted to the elasticsearch pod + SecretName string `json:"secretName,omitempty"` + // MountPath denotes the path to which the secret should be mounted to inside the elasticsearch pod + MountPath string `json:"mountPath,omitempty"` +} + func NewStatus(scp StackConfigPolicy) StackConfigPolicyStatus { status := StackConfigPolicyStatus{ ResourcesStatuses: map[string]ResourcePolicyStatus{}, @@ -168,21 +192,37 @@ func (s *StackConfigPolicyStatus) AddPolicyErrorFor(resource types.NamespacedNam return nil } -func (s *StackConfigPolicyStatus) UpdateResourceStatusPhase(resource types.NamespacedName, status ResourcePolicyStatus) { - if status.CurrentVersion == unknownVersion { //nolint:gocritic +func (s *StackConfigPolicyStatus) UpdateResourceStatusPhase(resource types.NamespacedName, status ResourcePolicyStatus, elasticsearchConfigAndMountsApplied bool) { + defer func() { + s.ResourcesStatuses[resource.String()] = status + s.Update() + }() + + if !elasticsearchConfigAndMountsApplied { + // New ElasticsearchConfig and Additional secrets not yet applied to the Elasticsearch pod + status.Phase = ApplyingChangesPhase + return + } + + if status.CurrentVersion == unknownVersion { status.Phase = UnknownPhase - } else if status.Error.Message != "" { + return + } + + if status.Error.Message != "" { status.Phase = ErrorPhase if status.ExpectedVersion > status.Error.Version { status.Phase = ApplyingChangesPhase } - } else if status.CurrentVersion == status.ExpectedVersion { + return + } + + if status.CurrentVersion == status.ExpectedVersion { status.Phase = ReadyPhase - } else { - status.Phase = ApplyingChangesPhase + return } - s.ResourcesStatuses[resource.String()] = status - s.Update() + + status.Phase = ApplyingChangesPhase } // Update updates the policy status from its resources statuses. diff --git a/pkg/apis/stackconfigpolicy/v1alpha1/webhook.go b/pkg/apis/stackconfigpolicy/v1alpha1/webhook.go index 9bfb1bf094f..e71736c6959 100644 --- a/pkg/apis/stackconfigpolicy/v1alpha1/webhook.go +++ b/pkg/apis/stackconfigpolicy/v1alpha1/webhook.go @@ -112,8 +112,32 @@ func validSettings(policy *StackConfigPolicy) field.ErrorList { if policy.Spec.Elasticsearch.IndexTemplates.ComposableIndexTemplates != nil { settingsCount += len(policy.Spec.Elasticsearch.IndexTemplates.ComposableIndexTemplates.Data) } + if policy.Spec.Elasticsearch.Config != nil { + settingsCount += len(policy.Spec.Elasticsearch.Config.Data) + } + if policy.Spec.Elasticsearch.SecretMounts != nil { + settingsCount += len(policy.Spec.Elasticsearch.SecretMounts) + } + // Check if mountpaths in the SecretMounts are unique + if !uniqueSecretMountPaths(policy.Spec.Elasticsearch.SecretMounts) { + return field.ErrorList{field.Invalid(field.NewPath("spec").Child("elasticsearch").Child("secretMounts"), policy.Spec.Elasticsearch.SecretMounts, "SecretMounts cannot have duplicate mount paths")} + } if settingsCount == 0 { return field.ErrorList{field.Required(field.NewPath("spec").Child("elasticsearch"), "Elasticsearch settings are mandatory and must not be empty")} } return nil } + +// uniqueSecretMountPaths returns true if all given mountpaths are unique +func uniqueSecretMountPaths(secretMounts []SecretMount) bool { + mountPathMap := make(map[string]bool) + + for _, secretMount := range secretMounts { + if _, ok := mountPathMap[secretMount.MountPath]; ok { + return false + } + mountPathMap[secretMount.MountPath] = true + } + + return true +} diff --git a/pkg/apis/stackconfigpolicy/v1alpha1/webhook_test.go b/pkg/apis/stackconfigpolicy/v1alpha1/webhook_test.go index 4c7f19bc487..9f06b9dbb9d 100644 --- a/pkg/apis/stackconfigpolicy/v1alpha1/webhook_test.go +++ b/pkg/apis/stackconfigpolicy/v1alpha1/webhook_test.go @@ -28,6 +28,16 @@ func TestWebhook(t *testing.T) { Object: func(t *testing.T, uid string) []byte { t.Helper() m := mkStackConfigPolicy(uid) + m.Spec.Elasticsearch.SecretMounts = []policyv1alpha1.SecretMount{ + { + SecretName: "test1", + MountPath: "/usr/test1", + }, + { + SecretName: "test2", + MountPath: "/usr/test2", + }, + } return serialize(t, m) }, Check: test.ValidationWebhookSucceeded, @@ -80,6 +90,28 @@ func TestWebhook(t *testing.T) { "Elasticsearch settings are mandatory and must not be empty", ), }, + { + Name: "create-duplicate-mountpaths", + Operation: admissionv1beta1.Create, + Object: func(t *testing.T, uid string) []byte { + t.Helper() + m := mkStackConfigPolicy(uid) + m.Spec.Elasticsearch.SecretMounts = []policyv1alpha1.SecretMount{ + { + SecretName: "test1", + MountPath: "/usr/test", + }, + { + SecretName: "test2", + MountPath: "/usr/test", + }, + } + return serialize(t, m) + }, + Check: test.ValidationWebhookFailed( + "SecretMounts cannot have duplicate mount paths", + ), + }, } validator := &policyv1alpha1.StackConfigPolicy{} diff --git a/pkg/apis/stackconfigpolicy/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/stackconfigpolicy/v1alpha1/zz_generated.deepcopy.go index 1bc6e9bce04..c1c92d89387 100644 --- a/pkg/apis/stackconfigpolicy/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/stackconfigpolicy/v1alpha1/zz_generated.deepcopy.go @@ -41,6 +41,15 @@ func (in *ElasticsearchConfigPolicySpec) DeepCopyInto(out *ElasticsearchConfigPo *out = (*in).DeepCopy() } in.IndexTemplates.DeepCopyInto(&out.IndexTemplates) + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = (*in).DeepCopy() + } + if in.SecretMounts != nil { + in, out := &in.SecretMounts, &out.SecretMounts + *out = make([]SecretMount, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ElasticsearchConfigPolicySpec. @@ -76,6 +85,30 @@ func (in *IndexTemplates) DeepCopy() *IndexTemplates { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KibanaConfigPolicySpec) DeepCopyInto(out *KibanaConfigPolicySpec) { + *out = *in + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = (*in).DeepCopy() + } + if in.SecretMounts != nil { + in, out := &in.SecretMounts, &out.SecretMounts + *out = make([]SecretMount, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KibanaConfigPolicySpec. +func (in *KibanaConfigPolicySpec) DeepCopy() *KibanaConfigPolicySpec { + if in == nil { + return nil + } + out := new(KibanaConfigPolicySpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PolicyStatusError) DeepCopyInto(out *PolicyStatusError) { *out = *in @@ -107,6 +140,21 @@ func (in *ResourcePolicyStatus) DeepCopy() *ResourcePolicyStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretMount) DeepCopyInto(out *SecretMount) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretMount. +func (in *SecretMount) DeepCopy() *SecretMount { + if in == nil { + return nil + } + out := new(SecretMount) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *StackConfigPolicy) DeepCopyInto(out *StackConfigPolicy) { *out = *in @@ -178,6 +226,7 @@ func (in *StackConfigPolicySpec) DeepCopyInto(out *StackConfigPolicySpec) { } } in.Elasticsearch.DeepCopyInto(&out.Elasticsearch) + in.Kibana.DeepCopyInto(&out.Kibana) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StackConfigPolicySpec. diff --git a/pkg/controller/common/labels/labels.go b/pkg/controller/common/labels/labels.go index 05281d2e579..1ff17401a62 100644 --- a/pkg/controller/common/labels/labels.go +++ b/pkg/controller/common/labels/labels.go @@ -12,6 +12,12 @@ import ( const ( credentialsLabel = "eck.k8s.elastic.co/credentials" //nolint:gosec + // StackConfigPolicyOnDeleteLabelName is a label used to indicate if an object should be reset or deleted on deletion of its stack config policy. + StackConfigPolicyOnDeleteLabelName = "asset.policy.k8s.elastic.co/on-delete" + // OrphanSecretResetOnPolicyDelete is used to set the data field of a secret to an empty map when the associated StackConfigPolicy or Elasticsearch is deleted. + OrphanSecretResetOnPolicyDelete = "reset" + // OrphanSecretResetOnPolicyDelete is used to delete the secret when the associated stackconfigpolicy or Elasticsearch is deleted. + OrphanObjectDeleteOnPolicyDelete = "delete" ) // TrueFalseLabel is a label that has a true/false value. diff --git a/pkg/controller/elasticsearch/driver/nodes.go b/pkg/controller/elasticsearch/driver/nodes.go index 7a770b88569..3c6069e5c9c 100644 --- a/pkg/controller/elasticsearch/driver/nodes.go +++ b/pkg/controller/elasticsearch/driver/nodes.go @@ -11,6 +11,8 @@ import ( "go.elastic.co/apm/v2" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" esv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/elasticsearch/v1" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/events" @@ -61,6 +63,16 @@ func (d *defaultDriver) reconcileNodeSpecs( return results.WithReconciliationState(defaultRequeue.WithReason(reason)) } + // Check for stack config policy Elasticsearch config secret + stackConfigPolicyConfigSecret := corev1.Secret{} + err = d.Client.Get(ctx, types.NamespacedName{ + Name: esv1.StackConfigElasticsearchConfigSecretName(d.ES.Name), + Namespace: d.ES.Namespace, + }, &stackConfigPolicyConfigSecret) + if err != nil && !apierrors.IsNotFound(err) { + return results.WithError(err) + } + // recreate any StatefulSet that needs to account for PVC expansion recreations, err := recreateStatefulSets(ctx, d.K8sClient(), d.ES) if err != nil { @@ -81,7 +93,7 @@ func (d *defaultDriver) reconcileNodeSpecs( return results.WithError(err) } - expectedResources, err := nodespec.BuildExpectedResources(ctx, d.Client, d.ES, keystoreResources, actualStatefulSets, d.OperatorParameters.IPFamily, d.OperatorParameters.SetDefaultSecurityContext) + expectedResources, err := nodespec.BuildExpectedResources(ctx, d.Client, d.ES, keystoreResources, actualStatefulSets, d.OperatorParameters.IPFamily, d.OperatorParameters.SetDefaultSecurityContext, stackConfigPolicyConfigSecret) if err != nil { return results.WithError(err) } diff --git a/pkg/controller/elasticsearch/filesettings/secret.go b/pkg/controller/elasticsearch/filesettings/secret.go index b10362116f6..70b9a9bb584 100644 --- a/pkg/controller/elasticsearch/filesettings/secret.go +++ b/pkg/controller/elasticsearch/filesettings/secret.go @@ -18,6 +18,7 @@ import ( commonv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/common/v1" esv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/elasticsearch/v1" policyv1alpha1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/stackconfigpolicy/v1alpha1" + commonlabel "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/labels" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/reconciler" eslabel "github.com/elastic/cloud-on-k8s/v2/pkg/controller/elasticsearch/label" "github.com/elastic/cloud-on-k8s/v2/pkg/utils/k8s" @@ -83,7 +84,7 @@ func NewSettingsSecret(version int64, es types.NamespacedName, currentSecret *co if policy != nil { // set this policy as soft owner of this Secret - setSoftOwner(settingsSecret, *policy) + SetSoftOwner(settingsSecret, *policy) // add the Secure Settings Secret sources to the Settings Secret if err := setSecureSettings(settingsSecret, *policy); err != nil { @@ -91,6 +92,9 @@ func NewSettingsSecret(version int64, es types.NamespacedName, currentSecret *co } } + // Add a label to reset secret on deletion of the stack config policy + settingsSecret.Labels[commonlabel.StackConfigPolicyOnDeleteLabelName] = commonlabel.OrphanSecretResetOnPolicyDelete + return *settingsSecret, version, nil } @@ -114,8 +118,8 @@ func hasChanged(settingsSecret corev1.Secret, newSettings Settings) bool { return settingsSecret.Annotations[settingsHashAnnotationName] != newSettings.hash() } -// setSoftOwner sets the given StackConfigPolicy as soft owner of the Settings Secret using the "softOwned" labels. -func setSoftOwner(settingsSecret *corev1.Secret, policy policyv1alpha1.StackConfigPolicy) { +// SetSoftOwner sets the given StackConfigPolicy as soft owner of the Settings Secret using the "softOwned" labels. +func SetSoftOwner(settingsSecret *corev1.Secret, policy policyv1alpha1.StackConfigPolicy) { if settingsSecret.Labels == nil { settingsSecret.Labels = map[string]string{} } diff --git a/pkg/controller/elasticsearch/filesettings/secret_test.go b/pkg/controller/elasticsearch/filesettings/secret_test.go index 46e8f74371b..2c4d73a5f0c 100644 --- a/pkg/controller/elasticsearch/filesettings/secret_test.go +++ b/pkg/controller/elasticsearch/filesettings/secret_test.go @@ -133,14 +133,14 @@ func Test_SettingsSecret_setSoftOwner_canBeOwnedBy(t *testing.T) { assert.Equal(t, true, canBeOwned) // set a policy soft owner - setSoftOwner(&secret, policy) + SetSoftOwner(&secret, policy) _, canBeOwned = CanBeOwnedBy(secret, policy) assert.Equal(t, true, canBeOwned) _, canBeOwned = CanBeOwnedBy(secret, otherPolicy) assert.Equal(t, false, canBeOwned) // update the policy soft owner - setSoftOwner(&secret, otherPolicy) + SetSoftOwner(&secret, otherPolicy) _, canBeOwned = CanBeOwnedBy(secret, policy) assert.Equal(t, false, canBeOwned) _, canBeOwned = CanBeOwnedBy(secret, otherPolicy) diff --git a/pkg/controller/elasticsearch/nodespec/podspec.go b/pkg/controller/elasticsearch/nodespec/podspec.go index e440c53025d..d790d877bf4 100644 --- a/pkg/controller/elasticsearch/nodespec/podspec.go +++ b/pkg/controller/elasticsearch/nodespec/podspec.go @@ -30,6 +30,7 @@ import ( "github.com/elastic/cloud-on-k8s/v2/pkg/controller/elasticsearch/stackmon" esvolume "github.com/elastic/cloud-on-k8s/v2/pkg/controller/elasticsearch/volume" "github.com/elastic/cloud-on-k8s/v2/pkg/utils/k8s" + "github.com/elastic/cloud-on-k8s/v2/pkg/utils/maps" "github.com/elastic/cloud-on-k8s/v2/pkg/utils/pointer" ) @@ -58,6 +59,7 @@ func BuildPodTemplateSpec( cfg settings.CanonicalConfig, keystoreResources *keystore.Resources, setDefaultSecurityContext bool, + policyConfig PolicyConfig, ) (corev1.PodTemplateSpec, error) { ver, err := version.Parse(es.Spec.Version) if err != nil { @@ -65,7 +67,7 @@ func BuildPodTemplateSpec( } downwardAPIVolume := volume.DownwardAPI{}.WithAnnotations(es.HasDownwardNodeLabels()) - volumes, volumeMounts := buildVolumes(es.Name, ver, nodeSet, keystoreResources, downwardAPIVolume) + volumes, volumeMounts := buildVolumes(es.Name, ver, nodeSet, keystoreResources, downwardAPIVolume, policyConfig.AdditionalVolumes) labels, err := buildLabels(es, cfg, nodeSet) if err != nil { @@ -99,7 +101,7 @@ func BuildPodTemplateSpec( if err := client.Get(context.Background(), types.NamespacedName{Namespace: es.Namespace, Name: esv1.ScriptsConfigMap(es.Name)}, esScripts); err != nil { return corev1.PodTemplateSpec{}, err } - annotations := buildAnnotations(es, cfg, keystoreResources, getScriptsConfigMapContent(esScripts)) + annotations := buildAnnotations(es, cfg, keystoreResources, getScriptsConfigMapContent(esScripts), policyConfig.PolicyAnnotations) // Attempt to detect if the default data directory is mounted in a volume. // If not, it could be a bug, a misconfiguration, or a custom storage configuration that requires the user to @@ -191,6 +193,7 @@ func buildAnnotations( cfg settings.CanonicalConfig, keystoreResources *keystore.Resources, scriptsContent string, + policyAnnotations map[string]string, ) map[string]string { // start from our defaults annotations := map[string]string{ @@ -216,6 +219,9 @@ func buildAnnotations( // set the annotation in place annotations[configHashAnnotationName] = fmt.Sprint(configHash.Sum32()) + // set policy annotations + maps.Merge(annotations, policyAnnotations) + return annotations } diff --git a/pkg/controller/elasticsearch/nodespec/podspec_test.go b/pkg/controller/elasticsearch/nodespec/podspec_test.go index d89639fde7c..6d03b9d5a7e 100644 --- a/pkg/controller/elasticsearch/nodespec/podspec_test.go +++ b/pkg/controller/elasticsearch/nodespec/podspec_test.go @@ -19,14 +19,18 @@ import ( commonv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/common/v1" esv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/elasticsearch/v1" + policyv1alpha1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/stackconfigpolicy/v1alpha1" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/defaults" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/hash" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/keystore" + common "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/settings" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/version" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/volume" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/elasticsearch/initcontainer" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/elasticsearch/settings" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/elasticsearch/user" esvolume "github.com/elastic/cloud-on-k8s/v2/pkg/controller/elasticsearch/volume" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/stackconfigpolicy" "github.com/elastic/cloud-on-k8s/v2/pkg/utils/k8s" "github.com/elastic/cloud-on-k8s/v2/pkg/utils/pointer" ) @@ -209,11 +213,11 @@ func TestBuildPodTemplateSpecWithDefaultSecurityContext(t *testing.T) { es.Spec.Version = tt.version.String() es.Spec.NodeSets[0].PodTemplate.Spec.SecurityContext = tt.userSecurityContext - cfg, err := settings.NewMergedESConfig(es.Name, tt.version, corev1.IPv4Protocol, es.Spec.HTTP, *es.Spec.NodeSets[0].Config) + cfg, err := settings.NewMergedESConfig(es.Name, tt.version, corev1.IPv4Protocol, es.Spec.HTTP, *es.Spec.NodeSets[0].Config, nil) require.NoError(t, err) client := k8s.NewFakeClient(&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: es.Namespace, Name: esv1.ScriptsConfigMap(es.Name)}}) - actual, err := BuildPodTemplateSpec(context.Background(), client, es, es.Spec.NodeSets[0], cfg, nil, tt.setDefaultFSGroup) + actual, err := BuildPodTemplateSpec(context.Background(), client, es, es.Spec.NodeSets[0], cfg, nil, tt.setDefaultFSGroup, PolicyConfig{}) require.NoError(t, err) require.Equal(t, tt.wantSecurityContext, actual.Spec.SecurityContext) }) @@ -225,11 +229,31 @@ func TestBuildPodTemplateSpec(t *testing.T) { nodeSet := sampleES.Spec.NodeSets[0] ver, err := version.Parse(sampleES.Spec.Version) require.NoError(t, err) - cfg, err := settings.NewMergedESConfig(sampleES.Name, ver, corev1.IPv4Protocol, sampleES.Spec.HTTP, *nodeSet.Config) + policyEsConfig := common.MustCanonicalConfig(map[string]interface{}{ + "logger.org.elasticsearch.discovery": "DEBUG", + }) + secretMounts := []policyv1alpha1.SecretMount{{ + SecretName: "test-es-secretname", + MountPath: "/usr/test", + }} + + elasticsearchConfigAndMountsHash := hash.HashObject([]interface{}{policyEsConfig, secretMounts}) + + policyConfig := PolicyConfig{ + ElasticsearchConfig: policyEsConfig, + AdditionalVolumes: []volume.VolumeLike{ + volume.NewSecretVolumeWithMountPath("test-es-secretname", "test-es-secretname", "/usr/test"), + }, + PolicyAnnotations: map[string]string{ + stackconfigpolicy.ElasticsearchConfigAndSecretMountsHashAnnotation: elasticsearchConfigAndMountsHash, + }, + } + + cfg, err := settings.NewMergedESConfig(sampleES.Name, ver, corev1.IPv4Protocol, sampleES.Spec.HTTP, *nodeSet.Config, policyConfig.ElasticsearchConfig) require.NoError(t, err) client := k8s.NewFakeClient(&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: sampleES.Namespace, Name: esv1.ScriptsConfigMap(sampleES.Name)}}) - actual, err := BuildPodTemplateSpec(context.Background(), client, sampleES, sampleES.Spec.NodeSets[0], cfg, nil, false) + actual, err := BuildPodTemplateSpec(context.Background(), client, sampleES, sampleES.Spec.NodeSets[0], cfg, nil, false, policyConfig) require.NoError(t, err) // build expected PodTemplateSpec @@ -237,7 +261,7 @@ func TestBuildPodTemplateSpec(t *testing.T) { terminationGracePeriodSeconds := DefaultTerminationGracePeriodSeconds varFalse := false - volumes, volumeMounts := buildVolumes(sampleES.Name, ver, nodeSet, nil, volume.DownwardAPI{}) + volumes, volumeMounts := buildVolumes(sampleES.Name, ver, nodeSet, nil, volume.DownwardAPI{}, policyConfig.AdditionalVolumes) // should be sorted sort.Slice(volumes, func(i, j int) bool { return volumes[i].Name < volumes[j].Name }) sort.Slice(volumeMounts, func(i, j int) bool { return volumeMounts[i].Name < volumeMounts[j].Name }) @@ -297,9 +321,10 @@ func TestBuildPodTemplateSpec(t *testing.T) { "pod-template-label-name": "pod-template-label-value", }, Annotations: map[string]string{ - "elasticsearch.k8s.elastic.co/config-hash": "533641620", - "pod-template-annotation-name": "pod-template-annotation-value", - "co.elastic.logs/module": "elasticsearch", + "elasticsearch.k8s.elastic.co/config-hash": "267866193", + "pod-template-annotation-name": "pod-template-annotation-value", + "co.elastic.logs/module": "elasticsearch", + "policy.k8s.elastic.co/elasticsearch-config-mounts-hash": "2095567618", }, }, Spec: corev1.PodSpec{ @@ -365,6 +390,7 @@ func Test_buildAnnotations(t *testing.T) { esAnnotations map[string]string keystoreResources *keystore.Resources scriptsContent string + policyAnnotations map[string]string } tests := []struct { name string @@ -445,15 +471,26 @@ func Test_buildAnnotations(t *testing.T) { "elasticsearch.k8s.elastic.co/config-hash": "1050348692", }, }, + { + name: "With policy annotations", + args: args{ + policyAnnotations: map[string]string{ + stackconfigpolicy.ElasticsearchConfigAndSecretMountsHashAnnotation: "testhash", + }, + }, + expectedAnnotations: map[string]string{ + stackconfigpolicy.ElasticsearchConfigAndSecretMountsHashAnnotation: "testhash", + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { es := newEsSampleBuilder().withKeystoreResources(tt.args.keystoreResources).withUserConfig(tt.args.cfg).addEsAnnotations(tt.args.esAnnotations).build() ver, err := version.Parse(sampleES.Spec.Version) require.NoError(t, err) - cfg, err := settings.NewMergedESConfig(es.Name, ver, corev1.IPv4Protocol, es.Spec.HTTP, *es.Spec.NodeSets[0].Config) + cfg, err := settings.NewMergedESConfig(es.Name, ver, corev1.IPv4Protocol, es.Spec.HTTP, *es.Spec.NodeSets[0].Config, nil) require.NoError(t, err) - got := buildAnnotations(es, cfg, tt.args.keystoreResources, tt.args.scriptsContent) + got := buildAnnotations(es, cfg, tt.args.keystoreResources, tt.args.scriptsContent, tt.args.policyAnnotations) for expectedAnnotation, expectedValue := range tt.expectedAnnotations { actualValue, exists := got[expectedAnnotation] @@ -548,10 +585,10 @@ func Test_enableLog4JFormatMsgNoLookups(t *testing.T) { ver, err := version.Parse(sampleES.Spec.Version) require.NoError(t, err) - cfg, err := settings.NewMergedESConfig(sampleES.Name, ver, corev1.IPv4Protocol, sampleES.Spec.HTTP, *sampleES.Spec.NodeSets[0].Config) + cfg, err := settings.NewMergedESConfig(sampleES.Name, ver, corev1.IPv4Protocol, sampleES.Spec.HTTP, *sampleES.Spec.NodeSets[0].Config, nil) require.NoError(t, err) client := k8s.NewFakeClient(&corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: sampleES.Namespace, Name: esv1.ScriptsConfigMap(sampleES.Name)}}) - actual, err := BuildPodTemplateSpec(context.Background(), client, sampleES, sampleES.Spec.NodeSets[0], cfg, nil, false) + actual, err := BuildPodTemplateSpec(context.Background(), client, sampleES, sampleES.Spec.NodeSets[0], cfg, nil, false, PolicyConfig{}) require.NoError(t, err) env := actual.Spec.Containers[1].Env diff --git a/pkg/controller/elasticsearch/nodespec/policy_config.go b/pkg/controller/elasticsearch/nodespec/policy_config.go new file mode 100644 index 00000000000..707106bbe46 --- /dev/null +++ b/pkg/controller/elasticsearch/nodespec/policy_config.go @@ -0,0 +1,77 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package nodespec + +import ( + "context" + "encoding/json" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + + esv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/elasticsearch/v1" + policyv1alpha1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/stackconfigpolicy/v1alpha1" + common "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/settings" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/volume" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/stackconfigpolicy" + "github.com/elastic/cloud-on-k8s/v2/pkg/utils/k8s" +) + +// PolicyConfig is a structure for storing Elasticsearch config from the StackConfigPolicy +type PolicyConfig struct { + ElasticsearchConfig *common.CanonicalConfig + PolicyAnnotations map[string]string + AdditionalVolumes []volume.VolumeLike +} + +// getPolicyConfig parses the StackConfigPolicy secret and returns a PolicyConfig struct +func getPolicyConfig(ctx context.Context, client k8s.Client, es esv1.Elasticsearch) (PolicyConfig, error) { + var policyConfig PolicyConfig + // Retrieve secret created by the StackConfigPolicy controller if it exists + // Check for stack config policy Elasticsearch config secret + stackConfigPolicyConfigSecret := corev1.Secret{} + err := client.Get(ctx, types.NamespacedName{ + Name: esv1.StackConfigElasticsearchConfigSecretName(es.Name), + Namespace: es.Namespace, + }, &stackConfigPolicyConfigSecret) + if err != nil && !apierrors.IsNotFound(err) { + return policyConfig, err + } + + // Additional annotations to be applied on the Elasticsearch pods + policyConfig.PolicyAnnotations = map[string]string{ + stackconfigpolicy.ElasticsearchConfigAndSecretMountsHashAnnotation: stackConfigPolicyConfigSecret.Annotations[stackconfigpolicy.ElasticsearchConfigAndSecretMountsHashAnnotation], + } + + // Parse Elasticsearch config from the stack config policy secret. + var esConfigFromStackConfigPolicy map[string]interface{} + if string(stackConfigPolicyConfigSecret.Data[stackconfigpolicy.ElasticSearchConfigKey]) != "" { + err = json.Unmarshal(stackConfigPolicyConfigSecret.Data[stackconfigpolicy.ElasticSearchConfigKey], &esConfigFromStackConfigPolicy) + if err != nil { + return policyConfig, err + } + } + canonicalConfig, err := common.NewCanonicalConfigFrom(esConfigFromStackConfigPolicy) + if err != nil { + return policyConfig, err + } + policyConfig.ElasticsearchConfig = canonicalConfig + + // Construct additional mounts for the Elasticsearch pods + var additionalSecretMounts []policyv1alpha1.SecretMount + if string(stackConfigPolicyConfigSecret.Data[stackconfigpolicy.SecretsMountKey]) != "" { + if err := json.Unmarshal(stackConfigPolicyConfigSecret.Data[stackconfigpolicy.SecretsMountKey], &additionalSecretMounts); err != nil { + return policyConfig, err + } + } + for _, secretMount := range additionalSecretMounts { + secretName := esv1.StackConfigAdditionalSecretName(es.Name, secretMount.SecretName) + secretVolumeFromStackConfigPolicy := volume.NewSecretVolumeWithMountPath(secretName, secretName, secretMount.MountPath) + policyConfig.AdditionalVolumes = append(policyConfig.AdditionalVolumes, secretVolumeFromStackConfigPolicy) + } + + return policyConfig, nil +} diff --git a/pkg/controller/elasticsearch/nodespec/policy_config_test.go b/pkg/controller/elasticsearch/nodespec/policy_config_test.go new file mode 100644 index 00000000000..cad12794abd --- /dev/null +++ b/pkg/controller/elasticsearch/nodespec/policy_config_test.go @@ -0,0 +1,119 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package nodespec + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + esv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/elasticsearch/v1" + common "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/settings" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/volume" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/stackconfigpolicy" + "github.com/elastic/cloud-on-k8s/v2/pkg/utils/k8s" +) + +func Test_getPolicyConfig(t *testing.T) { + canonicalConfig := common.MustCanonicalConfig(map[string]interface{}{ + "logger.org.elasticsearch.discovery": "DEBUG", + }) + for _, tt := range []struct { + name string + es esv1.Elasticsearch + configSecret corev1.Secret + want PolicyConfig + wantErr bool + }{ + { + name: "create valid policy config", + es: esv1.Elasticsearch{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-es", + Namespace: "test-ns", + }, + }, + configSecret: mkConfigSecret(esv1.StackConfigElasticsearchConfigSecretName("test-es"), "test-ns"), + want: PolicyConfig{ + ElasticsearchConfig: canonicalConfig, + PolicyAnnotations: map[string]string{ + "policy.k8s.elastic.co/elasticsearch-config-mounts-hash": "testhash", + }, + AdditionalVolumes: []volume.VolumeLike{ + volume.NewSecretVolumeWithMountPath(esv1.StackConfigAdditionalSecretName("test-es", "test1"), esv1.StackConfigAdditionalSecretName("test-es", "test1"), "/usr/test"), + }, + }, + }, + { + name: "create policy config when secret does not exist", + es: esv1.Elasticsearch{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-es", + Namespace: "test-ns", + }, + }, + want: PolicyConfig{ + ElasticsearchConfig: common.MustCanonicalConfig(map[string]interface{}{}), + PolicyAnnotations: map[string]string{ + "policy.k8s.elastic.co/elasticsearch-config-mounts-hash": "", + }, + AdditionalVolumes: nil, + }, + }, + { + name: "invalid config", + es: esv1.Elasticsearch{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-es", + Namespace: "test-ns", + }, + }, + configSecret: mkInvalidConfigSecret(esv1.StackConfigElasticsearchConfigSecretName("test-es"), "test-ns"), + want: PolicyConfig{ + ElasticsearchConfig: nil, + PolicyAnnotations: map[string]string{ + "policy.k8s.elastic.co/elasticsearch-config-mounts-hash": "testhash", + }, + AdditionalVolumes: nil, + }, + wantErr: true, + }, + } { + t.Run(tt.name, func(t *testing.T) { + client := k8s.NewFakeClient(&tt.configSecret) + got, err := getPolicyConfig(context.Background(), client, tt.es) + if !tt.wantErr { + require.NoError(t, err) + } else { + require.Error(t, err) + } + require.Equal(t, tt.want, got) + }) + } +} + +func mkConfigSecret(name string, namespace string) corev1.Secret { + return corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + stackconfigpolicy.ElasticsearchConfigAndSecretMountsHashAnnotation: "testhash", + }, + }, + Data: map[string][]byte{stackconfigpolicy.ElasticSearchConfigKey: []byte(`{"logger.org.elasticsearch.discovery": "DEBUG"}`), + stackconfigpolicy.SecretsMountKey: []byte(`[{"secretName": "test1", "mountPath": "/usr/test"}]`)}, + } +} + +func mkInvalidConfigSecret(name string, namespace string) corev1.Secret { + secret := mkConfigSecret(name, namespace) + secret.Data = map[string][]byte{stackconfigpolicy.ElasticSearchConfigKey: []byte(`{"invalid"}`), + stackconfigpolicy.SecretsMountKey: []byte(`[{"secretName": "test1", "mountPath": "/usr/test"}]`)} + return secret +} diff --git a/pkg/controller/elasticsearch/nodespec/resources.go b/pkg/controller/elasticsearch/nodespec/resources.go index d48ce4d5ee9..87d118abeb8 100644 --- a/pkg/controller/elasticsearch/nodespec/resources.go +++ b/pkg/controller/elasticsearch/nodespec/resources.go @@ -60,6 +60,7 @@ func BuildExpectedResources( existingStatefulSets sset.StatefulSetList, ipFamily corev1.IPFamily, setDefaultSecurityContext bool, + stackConfigPolicyConfigSecret corev1.Secret, ) (ResourcesList, error) { nodesResources := make(ResourcesList, 0, len(es.Spec.NodeSets)) @@ -68,19 +69,25 @@ func BuildExpectedResources( return nil, err } + // Get policy config from StackConfigPolicy + policyConfig, err := getPolicyConfig(ctx, client, es) + if err != nil { + return nil, err + } + for _, nodeSpec := range es.Spec.NodeSets { // build es config userCfg := commonv1.Config{} if nodeSpec.Config != nil { userCfg = *nodeSpec.Config } - cfg, err := settings.NewMergedESConfig(es.Name, ver, ipFamily, es.Spec.HTTP, userCfg) + cfg, err := settings.NewMergedESConfig(es.Name, ver, ipFamily, es.Spec.HTTP, userCfg, policyConfig.ElasticsearchConfig) if err != nil { return nil, err } // build stateful set and associated headless service - statefulSet, err := BuildStatefulSet(ctx, client, es, nodeSpec, cfg, keystoreResources, existingStatefulSets, setDefaultSecurityContext) + statefulSet, err := BuildStatefulSet(ctx, client, es, nodeSpec, cfg, keystoreResources, existingStatefulSets, setDefaultSecurityContext, policyConfig) if err != nil { return nil, err } diff --git a/pkg/controller/elasticsearch/nodespec/statefulset.go b/pkg/controller/elasticsearch/nodespec/statefulset.go index cb92f866f5e..6f8a2c8e854 100644 --- a/pkg/controller/elasticsearch/nodespec/statefulset.go +++ b/pkg/controller/elasticsearch/nodespec/statefulset.go @@ -65,6 +65,7 @@ func BuildStatefulSet( keystoreResources *keystore.Resources, existingStatefulSets sset.StatefulSetList, setDefaultSecurityContext bool, + policyConfig PolicyConfig, ) (appsv1.StatefulSet, error) { statefulSetName := esv1.StatefulSet(es.Name, nodeSet.Name) @@ -79,7 +80,7 @@ func BuildStatefulSet( ) // build pod template - podTemplate, err := BuildPodTemplateSpec(ctx, client, es, nodeSet, cfg, keystoreResources, setDefaultSecurityContext) + podTemplate, err := BuildPodTemplateSpec(ctx, client, es, nodeSet, cfg, keystoreResources, setDefaultSecurityContext, policyConfig) if err != nil { return appsv1.StatefulSet{}, err } diff --git a/pkg/controller/elasticsearch/nodespec/volumes.go b/pkg/controller/elasticsearch/nodespec/volumes.go index 2e230da7bad..3e6e9e69616 100644 --- a/pkg/controller/elasticsearch/nodespec/volumes.go +++ b/pkg/controller/elasticsearch/nodespec/volumes.go @@ -25,6 +25,7 @@ func buildVolumes( nodeSpec esv1.NodeSet, keystoreResources *keystore.Resources, downwardAPIVolume volume.DownwardAPI, + additionalMountsFromPolicy []volume.VolumeLike, ) ([]corev1.Volume, []corev1.VolumeMount) { configVolume := settings.ConfigSecretVolume(esv1.StatefulSet(esName, nodeSpec.Name)) probeSecret := volume.NewSelectiveSecretVolumeWithMountPath( @@ -120,6 +121,12 @@ func buildVolumes( volumeMounts = append(volumeMounts, fileSettingsVolume.VolumeMount()) } + // additional volumes from stack config policy + for _, volume := range additionalMountsFromPolicy { + volumes = append(volumes, volume.Volume()) + volumeMounts = append(volumeMounts, volume.VolumeMount()) + } + // include the user-provided PodTemplate volumes as the user may have defined the data volume there (e.g.: emptyDir or hostpath volume) volumeMounts = esvolume.AppendDefaultDataVolumeMount(volumeMounts, append(volumes, nodeSpec.PodTemplate.Spec.Volumes...)) diff --git a/pkg/controller/elasticsearch/nodespec/volumes_test.go b/pkg/controller/elasticsearch/nodespec/volumes_test.go index 8884834d775..0f42fcec2c7 100644 --- a/pkg/controller/elasticsearch/nodespec/volumes_test.go +++ b/pkg/controller/elasticsearch/nodespec/volumes_test.go @@ -90,7 +90,7 @@ func Test_BuildVolumes_DataVolumeMountPath(t *testing.T) { for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { - _, volumeMounts := buildVolumes("esname", version.MustParse("8.8.0"), tc.nodeSpec, nil, volume.DownwardAPI{}) + _, volumeMounts := buildVolumes("esname", version.MustParse("8.8.0"), tc.nodeSpec, nil, volume.DownwardAPI{}, []volume.VolumeLike{}) assert.True(t, contains(volumeMounts, "elasticsearch-data", "/usr/share/elasticsearch/data")) }) } diff --git a/pkg/controller/elasticsearch/settings/merged_config.go b/pkg/controller/elasticsearch/settings/merged_config.go index 3369d9d1075..788e28fdbb7 100644 --- a/pkg/controller/elasticsearch/settings/merged_config.go +++ b/pkg/controller/elasticsearch/settings/merged_config.go @@ -33,15 +33,18 @@ func NewMergedESConfig( ipFamily corev1.IPFamily, httpConfig commonv1.HTTPConfig, userConfig commonv1.Config, + esConfigFromStackConfigPolicy *common.CanonicalConfig, ) (CanonicalConfig, error) { userCfg, err := common.NewCanonicalConfigFrom(userConfig.Data) if err != nil { return CanonicalConfig{}, err } + config := baseConfig(clusterName, ver, ipFamily).CanonicalConfig err = config.MergeWith( xpackConfig(ver, httpConfig).CanonicalConfig, userCfg, + esConfigFromStackConfigPolicy, ) if err != nil { return CanonicalConfig{}, err diff --git a/pkg/controller/elasticsearch/settings/merged_config_test.go b/pkg/controller/elasticsearch/settings/merged_config_test.go index 955b970a39e..d443aa349bb 100644 --- a/pkg/controller/elasticsearch/settings/merged_config_test.go +++ b/pkg/controller/elasticsearch/settings/merged_config_test.go @@ -14,6 +14,7 @@ import ( commonv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/common/v1" esv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/elasticsearch/v1" + common "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/settings" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/version" ) @@ -36,12 +37,17 @@ func TestNewMergedESConfig(t *testing.T) { } `yaml:"network"` } + policyCfg := common.MustCanonicalConfig(map[string]interface{}{ + esv1.DiscoverySeedProviders: "policy-override", + }) + tests := []struct { - name string - version string - ipFamily corev1.IPFamily - cfgData map[string]interface{} - assert func(cfg CanonicalConfig) + name string + version string + ipFamily corev1.IPFamily + cfgData map[string]interface{} + policyCfgData *common.CanonicalConfig + assert func(cfg CanonicalConfig) }{ { name: "in 6.x, empty config should have the default file and native realm settings configured", @@ -195,6 +201,27 @@ func TestNewMergedESConfig(t *testing.T) { require.Equal(t, 1, bytes.Count(cfgBytes, []byte("seed_providers:"))) }, }, + { + name: "Elasticsearch config overrides from policy should have precedence over default config", + version: "7.6.0", + ipFamily: corev1.IPv4Protocol, + cfgData: map[string]interface{}{ + esv1.DiscoverySeedProviders: "something-else", + }, + policyCfgData: policyCfg, + assert: func(cfg CanonicalConfig) { + cfgBytes, err := cfg.Render() + require.NoError(t, err) + esCfg := &elasticsearchCfg{} + require.NoError(t, yaml.Unmarshal(cfgBytes, &esCfg)) + // default config is still there + require.Equal(t, "${POD_IP}", esCfg.Network.PublishHost) + require.Equal(t, "${POD_NAME}.${HEADLESS_SERVICE_NAME}.${NAMESPACE}.svc", esCfg.HTTP.PublishHost) + // but has been overridden + require.Equal(t, "policy-override", esCfg.Discovery.SeedProviders) + require.Equal(t, 1, bytes.Count(cfgBytes, []byte("seed_providers:"))) + }, + }, { name: "configuration is adjusted for IP family", version: "7.6.0", @@ -214,7 +241,7 @@ func TestNewMergedESConfig(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ver, err := version.Parse(tt.version) require.NoError(t, err) - cfg, err := NewMergedESConfig("clusterName", ver, tt.ipFamily, commonv1.HTTPConfig{}, commonv1.Config{Data: tt.cfgData}) + cfg, err := NewMergedESConfig("clusterName", ver, tt.ipFamily, commonv1.HTTPConfig{}, commonv1.Config{Data: tt.cfgData}, tt.policyCfgData) require.NoError(t, err) tt.assert(cfg) }) diff --git a/pkg/controller/stackconfigpolicy/controller.go b/pkg/controller/stackconfigpolicy/controller.go index 50ef806dbeb..a6abe664980 100644 --- a/pkg/controller/stackconfigpolicy/controller.go +++ b/pkg/controller/stackconfigpolicy/controller.go @@ -31,6 +31,7 @@ import ( "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common" commonesclient "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/esclient" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/events" + commonlabels "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/labels" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/license" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/operator" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/reconciler" @@ -291,6 +292,35 @@ func (r *ReconcileStackConfigPolicy) doReconcile(ctx context.Context, policy pol return results.WithError(err), status } + // Copy all the Secrets that are present in spec.elasticsearch.secretMounts + if err := reconcileSecretMounts(ctx, r.Client, es, &policy); err != nil { + if apierrors.IsNotFound(err) { + err = status.AddPolicyErrorFor(esNsn, policyv1alpha1.ErrorPhase, err.Error()) + if err != nil { + return results.WithError(err), status + } + results.WithResult(defaultRequeue) + } + continue + } + + // create expected elasticsearch config secret + expectedConfigSecret, err := newElasticsearchConfigSecret(policy, es) + if err != nil { + return results.WithError(err), status + } + + _, err = reconciler.ReconcileSecret(ctx, r.Client, expectedConfigSecret, &es) + if err != nil { + return results.WithError(err), status + } + + // Check if required Elasticsearch config and secret mounts are applied. + configAndSecretMountsApplied, err := elasticsearchConfigAndSecretMountsApplied(ctx, r.Client, policy, es) + if err != nil { + return results.WithError(err), status + } + // get /_cluster/state to get the Settings currently configured in ES currentSettings, err := r.getClusterStateFileSettings(ctx, es) if err != nil { @@ -303,11 +333,11 @@ func (r *ReconcileStackConfigPolicy) doReconcile(ctx context.Context, policy pol } // update the ES resource status for this ES - status.UpdateResourceStatusPhase(esNsn, newResourceStatus(currentSettings, expectedVersion)) + status.UpdateResourceStatusPhase(esNsn, newResourceStatus(currentSettings, expectedVersion), configAndSecretMountsApplied) } - // reset Settings secrets for resources no longer selected by this policy - results.WithError(resetOrphanSoftOwnedSecrets(ctx, r.Client, k8s.ExtractNamespacedName(&policy), configuredResources)) + // reset/delete Settings secrets for resources no longer selected by this policy + results.WithError(handleOrphanSoftOwnedSecrets(ctx, r.Client, k8s.ExtractNamespacedName(&policy), configuredResources)) // requeue if not ready if status.Phase != policyv1alpha1.ReadyPhase { @@ -384,10 +414,18 @@ func (r *ReconcileStackConfigPolicy) updateStatus(ctx context.Context, scp polic } func (r *ReconcileStackConfigPolicy) onDelete(ctx context.Context, obj types.NamespacedName) error { - return resetOrphanSoftOwnedSecrets(ctx, r.Client, obj, nil) + return handleOrphanSoftOwnedSecrets(ctx, r.Client, obj, nil) +} + +func handleOrphanSoftOwnedSecrets(ctx context.Context, c k8s.Client, softOwner types.NamespacedName, configuredEs esMap) error { + err := resetOrphanSoftOwnedSecrets(ctx, c, softOwner, configuredEs) + if err != nil { + return err + } + return deleteOrphanSoftOwnedSecrets(ctx, c, softOwner, configuredEs) } -// resetOrphanSoftOwnedSecrets resets the File settings secrets for the Elasticsearch clusters that are no longer configured +// resetOrphanSoftOwnedSecrets resets secrets for the Elasticsearch clusters that are no longer configured // by a given StackConfigPolicy. // An optional list of Elasticsearch currently configured by the policy can be provided to filter secrets not to be modified. Without list, // all secrets soft owned by the policy are reset. @@ -399,9 +437,10 @@ func resetOrphanSoftOwnedSecrets(ctx context.Context, c k8s.Client, softOwner ty // search in all namespaces // restrict to secrets on which we set the soft owner labels client.MatchingLabels{ - reconciler.SoftOwnerNamespaceLabel: softOwner.Namespace, - reconciler.SoftOwnerNameLabel: softOwner.Name, - reconciler.SoftOwnerKindLabel: policyv1alpha1.Kind, + reconciler.SoftOwnerNamespaceLabel: softOwner.Namespace, + reconciler.SoftOwnerNameLabel: softOwner.Name, + reconciler.SoftOwnerKindLabel: policyv1alpha1.Kind, + commonlabels.StackConfigPolicyOnDeleteLabelName: commonlabels.OrphanSecretResetOnPolicyDelete, }, ); err != nil { return err @@ -437,6 +476,45 @@ func resetOrphanSoftOwnedSecrets(ctx context.Context, c k8s.Client, softOwner ty return nil } +// deleteOrphanSoftOwnedSecrets deletes secrets for the Elasticsearch clusters that are no longer configured +// by a given StackConfigPolicy. +func deleteOrphanSoftOwnedSecrets(ctx context.Context, c k8s.Client, softOwner types.NamespacedName, configuredEs esMap) error { + var secrets corev1.SecretList + if err := c.List(ctx, + &secrets, + // search in all namespaces + // restrict to secrets on which we set the soft owner labels + client.MatchingLabels{ + reconciler.SoftOwnerNamespaceLabel: softOwner.Namespace, + reconciler.SoftOwnerNameLabel: softOwner.Name, + reconciler.SoftOwnerKindLabel: policyv1alpha1.Kind, + commonlabels.StackConfigPolicyOnDeleteLabelName: commonlabels.OrphanObjectDeleteOnPolicyDelete, + }, + ); err != nil { + return err + } + + for i := range secrets.Items { + secret := secrets.Items[i] + esNsn := types.NamespacedName{ + Namespace: secret.Namespace, + Name: secret.Labels[eslabel.ClusterNameLabelName], + } + _, exist := configuredEs[esNsn] + if exist { + continue + } + + // given elasticsearchcluster is no longer managed by stack config policy, delete secret. + err := c.Delete(ctx, &secret) + if err != nil && !apierrors.IsNotFound(err) { + return err + } + } + + return nil +} + // getClusterStateFileSettings gets the file based settings currently configured in an Elasticsearch by calling the /_cluster/state API. func (r *ReconcileStackConfigPolicy) getClusterStateFileSettings(ctx context.Context, es esv1.Elasticsearch) (esclient.FileSettings, error) { span, _ := apm.StartSpan(ctx, "get_cluster_state", tracing.SpanTypeApp) @@ -454,3 +532,23 @@ func (r *ReconcileStackConfigPolicy) getClusterStateFileSettings(ctx context.Con return clusterState.Metadata.ReservedState.FileSettings, nil } + +// elasticsearchConfigAndSecretMountsApplied checks if the elasticsearch config and secret mounts from the stack config policy have been applied to the Elasticsearch cluster. +func elasticsearchConfigAndSecretMountsApplied(ctx context.Context, c k8s.Client, policy policyv1alpha1.StackConfigPolicy, es esv1.Elasticsearch) (bool, error) { + // Get Pods for the given Elasticsearch + podList := corev1.PodList{} + if err := c.List(ctx, &podList, client.MatchingLabels{ + eslabel.ClusterNameLabelName: es.Name, + }); err != nil || len(podList.Items) == 0 { + return false, err + } + + elasticsearchAndMountsConfigHash := getElasticsearchConfigAndMountsHash(policy.Spec.Elasticsearch.Config, policy.Spec.Elasticsearch.SecretMounts) + for _, esPod := range podList.Items { + if esPod.Annotations[ElasticsearchConfigAndSecretMountsHashAnnotation] != elasticsearchAndMountsConfigHash { + return false, nil + } + } + + return true, nil +} diff --git a/pkg/controller/stackconfigpolicy/controller_test.go b/pkg/controller/stackconfigpolicy/controller_test.go index f4df96e82a3..7438fded3a5 100644 --- a/pkg/controller/stackconfigpolicy/controller_test.go +++ b/pkg/controller/stackconfigpolicy/controller_test.go @@ -12,9 +12,11 @@ import ( "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" commonv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/common/v1" @@ -22,9 +24,12 @@ import ( policyv1alpha1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/stackconfigpolicy/v1alpha1" commonesclient "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/esclient" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/hash" + commonlabels "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/labels" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/license" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/reconciler" esclient "github.com/elastic/cloud-on-k8s/v2/pkg/controller/elasticsearch/client" "github.com/elastic/cloud-on-k8s/v2/pkg/controller/elasticsearch/filesettings" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/elasticsearch/label" "github.com/elastic/cloud-on-k8s/v2/pkg/utils/k8s" "github.com/elastic/cloud-on-k8s/v2/pkg/utils/net" ) @@ -83,6 +88,19 @@ func fetchEvents(recorder *record.FakeRecorder) []string { return events } +func getEsPod(namespace string, annotations map[string]string) *corev1.Pod { + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-es-default-0", + Namespace: "ns", + Labels: map[string]string{ + label.ClusterNameLabelName: "test-es", + }, + Annotations: annotations, + }, + } +} + func TestReconcileStackConfigPolicy_Reconcile(t *testing.T) { nsnFixture := types.NamespacedName{ Namespace: "ns", @@ -99,9 +117,24 @@ func TestReconcileStackConfigPolicy_Reconcile(t *testing.T) { ClusterSettings: &commonv1.Config{Data: map[string]interface{}{ "indices.recovery.max_bytes_per_sec": "42mb", }}, + SecretMounts: []policyv1alpha1.SecretMount{ + { + SecretName: "test-secret-mount", + MountPath: "/usr/test", + }, + }, + Config: &commonv1.Config{ + Data: map[string]interface{}{ + "logger.org.elasticsearch.discovery": "DEBUG", + }, + }, }, }, } + elasticsearchConfigAndMountsHash := getElasticsearchConfigAndMountsHash(policyFixture.Spec.Elasticsearch.Config, policyFixture.Spec.Elasticsearch.SecretMounts) + esPodFixture := getEsPod("ns", map[string]string{ + ElasticsearchConfigAndSecretMountsHashAnnotation: elasticsearchConfigAndMountsHash, + }) esFixture := esv1.Elasticsearch{ObjectMeta: metav1.ObjectMeta{ Namespace: "ns", Name: "test-es", @@ -109,16 +142,18 @@ func TestReconcileStackConfigPolicy_Reconcile(t *testing.T) { }, Spec: esv1.ElasticsearchSpec{Version: "8.6.1"}, } + secretMountsSecretFixture := getSecretMountSecret(t, "test-secret-mount", "ns", "test-policy", "ns", "delete") secretFixture := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: "ns", Name: "test-es-es-file-settings", Labels: map[string]string{ - "common.k8s.elastic.co/type": "elasticsearch", - "elasticsearch.k8s.elastic.co/cluster-name": "test-es", - "eck.k8s.elastic.co/owner-kind": "StackConfigPolicy", - "eck.k8s.elastic.co/owner-namespace": "ns", - "eck.k8s.elastic.co/owner-name": "test-policy", + "common.k8s.elastic.co/type": "elasticsearch", + "elasticsearch.k8s.elastic.co/cluster-name": "test-es", + "eck.k8s.elastic.co/owner-kind": "StackConfigPolicy", + "eck.k8s.elastic.co/owner-namespace": "ns", + "eck.k8s.elastic.co/owner-name": "test-policy", + commonlabels.StackConfigPolicyOnDeleteLabelName: commonlabels.OrphanSecretResetOnPolicyDelete, }, }, Data: map[string][]byte{"settings.json": []byte(`{"metadata":{"version":"42","compatibility":"8.4.0"},"state":{"cluster_settings":{"indices.recovery.max_bytes_per_sec":"42mb"},"snapshot_repositories":{},"slm":{},"role_mappings":{},"autoscaling":{},"ilm":{},"ingest_pipelines":{},"index_templates":{"component_templates":{},"composable_index_templates":{}}}}`)}, @@ -134,6 +169,22 @@ func TestReconcileStackConfigPolicy_Reconcile(t *testing.T) { orphanSecretFixture.Name = "another-es-es-file-settings" orphanSecretFixture.Labels["elasticsearch.k8s.elastic.co/cluster-name"] = "another-es" + orphanElasticsearchConfigSecretFixture := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: esv1.StackConfigElasticsearchConfigSecretName("another-es"), + Namespace: "ns", + Labels: map[string]string{ + "elasticsearch.k8s.elastic.co/cluster-name": "another-es", + commonlabels.StackConfigPolicyOnDeleteLabelName: commonlabels.OrphanObjectDeleteOnPolicyDelete, + reconciler.SoftOwnerNamespaceLabel: policyFixture.Namespace, + reconciler.SoftOwnerNameLabel: policyFixture.Name, + reconciler.SoftOwnerKindLabel: policyv1alpha1.Kind, + }, + }, + } + + orphanSecretMountsSecretFixture := getSecretMountSecret(t, esv1.ESNamer.Suffix("another-es", "test-secret-mount"), "ns", "test-policy", "ns", "delete") + updatedPolicyFixture := policyFixture.DeepCopy() updatedPolicyFixture.Spec.Elasticsearch.ClusterSettings = &commonv1.Config{Data: map[string]interface{}{ "indices.recovery.max_bytes_per_sec": "43mb", @@ -202,10 +253,10 @@ func TestReconcileStackConfigPolicy_Reconcile(t *testing.T) { { name: "Reset settings secret on StackConfigPolicy deletion", args: args{ - client: k8s.NewFakeClient(&esFixture, &secretFixture), + client: k8s.NewFakeClient(&esFixture, &secretFixture, secretMountsSecretFixture), }, pre: func(r ReconcileStackConfigPolicy) { - // after the reconciliation, settings are empty + // before the reconciliation, settings are not empty settings := r.getSettings(t, k8s.ExtractNamespacedName(&secretFixture)) assert.NotEmpty(t, settings.State.ClusterSettings.Data) }, @@ -219,7 +270,26 @@ func TestReconcileStackConfigPolicy_Reconcile(t *testing.T) { { name: "Reset orphan soft owned secrets when an Elasticsearch is no more configured by a StackConfigPolicy", args: args{ - client: k8s.NewFakeClient(&policyFixture, &esFixture, &secretFixture, orphanSecretFixture, orphanEsFixture), + client: k8s.NewFakeClient(&policyFixture, &esFixture, &secretFixture, orphanSecretFixture, orphanEsFixture, secretMountsSecretFixture, esPodFixture), + licenseChecker: &license.MockLicenseChecker{EnterpriseEnabled: true}, + esClientProvider: fakeClientProvider(clusterStateFileSettingsFixture(42, nil), nil), + }, + pre: func(r ReconcileStackConfigPolicy) { + // before the reconciliation, settings are not empty + settings := r.getSettings(t, k8s.ExtractNamespacedName(orphanSecretFixture)) + assert.NotEmpty(t, settings.State.ClusterSettings) + }, + post: func(r ReconcileStackConfigPolicy, recorder record.FakeRecorder) { + // after the reconciliation, settings are empty + settings := r.getSettings(t, k8s.ExtractNamespacedName(orphanSecretFixture)) + assert.Empty(t, settings.State.ClusterSettings.Data) + }, + wantErr: false, + }, + { + name: "Reset orphan soft owned secrets when the stackconfigpolicy no longer exists", + args: args{ + client: k8s.NewFakeClient(&esFixture, &secretFixture, orphanSecretFixture, orphanEsFixture, secretMountsSecretFixture, esPodFixture), licenseChecker: &license.MockLicenseChecker{EnterpriseEnabled: true}, esClientProvider: fakeClientProvider(clusterStateFileSettingsFixture(42, nil), nil), }, @@ -275,7 +345,7 @@ func TestReconcileStackConfigPolicy_Reconcile(t *testing.T) { { name: "Elasticsearch cluster in old version without support for file based settings", args: args{ - client: k8s.NewFakeClient(&policyFixture, &secretFixture, &oldVersionEsFixture), + client: k8s.NewFakeClient(&policyFixture, &secretFixture, &oldVersionEsFixture, esPodFixture), licenseChecker: &license.MockLicenseChecker{EnterpriseEnabled: true}, }, post: func(r ReconcileStackConfigPolicy, recorder record.FakeRecorder) { @@ -294,7 +364,7 @@ func TestReconcileStackConfigPolicy_Reconcile(t *testing.T) { { name: "Elasticsearch cluster is unreachable", args: args{ - client: k8s.NewFakeClient(&policyFixture, &esFixture, &secretFixture), + client: k8s.NewFakeClient(&policyFixture, &esFixture, &secretFixture, secretMountsSecretFixture, esPodFixture), licenseChecker: &license.MockLicenseChecker{EnterpriseEnabled: true}, esClientProvider: fakeClientProvider(esclient.FileSettings{}, errors.New("elasticsearch client failed")), }, @@ -312,7 +382,7 @@ func TestReconcileStackConfigPolicy_Reconcile(t *testing.T) { { name: "Settings secret must be updated to reflect the policy settings", args: args{ - client: k8s.NewFakeClient(updatedPolicyFixture, &esFixture, &secretFixture), + client: k8s.NewFakeClient(updatedPolicyFixture, &esFixture, &secretFixture, secretMountsSecretFixture, esPodFixture), licenseChecker: &license.MockLicenseChecker{EnterpriseEnabled: true}, esClientProvider: fakeClientProvider(clusterStateFileSettingsFixture(43, nil), nil), }, @@ -341,7 +411,7 @@ func TestReconcileStackConfigPolicy_Reconcile(t *testing.T) { { name: "Current settings are wrong", args: args{ - client: k8s.NewFakeClient(&policyFixture, &esFixture, &secretFixture), + client: k8s.NewFakeClient(&policyFixture, &esFixture, &secretFixture, secretMountsSecretFixture, esPodFixture), licenseChecker: &license.MockLicenseChecker{EnterpriseEnabled: true}, esClientProvider: fakeClientProvider(clusterStateFileSettingsFixture(42, errors.New("invalid cluster settings")), nil), }, @@ -364,7 +434,7 @@ func TestReconcileStackConfigPolicy_Reconcile(t *testing.T) { { name: "Current settings version is different from the expected one", args: args{ - client: k8s.NewFakeClient(&policyFixture, &esFixture, &secretFixture), + client: k8s.NewFakeClient(&policyFixture, &esFixture, &secretFixture, secretMountsSecretFixture, esPodFixture), licenseChecker: &license.MockLicenseChecker{EnterpriseEnabled: true}, esClientProvider: fakeClientProvider(clusterStateFileSettingsFixture(40, nil), nil), }, @@ -386,7 +456,7 @@ func TestReconcileStackConfigPolicy_Reconcile(t *testing.T) { { name: "Happy path", args: args{ - client: k8s.NewFakeClient(&policyFixture, &esFixture, &secretFixture), + client: k8s.NewFakeClient(&policyFixture, &esFixture, &secretFixture, secretMountsSecretFixture, esPodFixture), licenseChecker: &license.MockLicenseChecker{EnterpriseEnabled: true}, esClientProvider: fakeClientProvider(clusterStateFileSettingsFixture(42, nil), nil), }, @@ -400,11 +470,73 @@ func TestReconcileStackConfigPolicy_Reconcile(t *testing.T) { assert.Equal(t, 1, policy.Status.Resources) assert.Equal(t, 1, policy.Status.Ready) assert.Equal(t, policyv1alpha1.ReadyPhase, policy.Status.Phase) + var esSecret corev1.Secret + // Verify the config secret created by the stack config policy controller + err = r.Client.Get(context.Background(), types.NamespacedName{ + Namespace: "ns", + Name: esv1.StackConfigElasticsearchConfigSecretName(esFixture.Name), + }, &esSecret) + assert.NoError(t, err) + elasticsearchConfigJSONData, err := json.Marshal(policy.Spec.Elasticsearch.Config) + assert.NoError(t, err) + secretMountsJSONData, err := json.Marshal(policy.Spec.Elasticsearch.SecretMounts) + assert.NoError(t, err) + assert.Equal(t, esSecret.Data[ElasticSearchConfigKey], elasticsearchConfigJSONData) + assert.Equal(t, esSecret.Data[SecretsMountKey], secretMountsJSONData) + + // Verify the secret mounts secret + assertExpectedESSecretContent(t, r.Client, esFixture.Name, *secretMountsSecretFixture, policy.Spec.Elasticsearch.SecretMounts) }, wantErr: false, wantRequeue: false, wantRequeueAfter: false, }, + { + name: "Delete orphan soft owned elasticsearch config and secret mounts secrets when an Elasticsearch is no more configured by a StackConfigPolicy", + args: args{ + client: k8s.NewFakeClient(&policyFixture, &esFixture, &secretFixture, orphanSecretFixture, orphanEsFixture, secretMountsSecretFixture, esPodFixture, &orphanElasticsearchConfigSecretFixture, orphanSecretMountsSecretFixture), + licenseChecker: &license.MockLicenseChecker{EnterpriseEnabled: true}, + esClientProvider: fakeClientProvider(clusterStateFileSettingsFixture(42, nil), nil), + }, + pre: func(r ReconcileStackConfigPolicy) { + // before the reconciliation, settings are not empty + settings := r.getSettings(t, k8s.ExtractNamespacedName(orphanSecretFixture)) + assert.NotEmpty(t, settings.State.ClusterSettings) + + // before the reconciliation, settings exist + var configSecret, secretMountsSecret corev1.Secret + err := r.Client.Get(context.Background(), types.NamespacedName{ + Name: esv1.StackConfigElasticsearchConfigSecretName("another-es"), + Namespace: "ns", + }, &configSecret) + assert.NoError(t, err) + err = r.Client.Get(context.Background(), types.NamespacedName{ + Name: esv1.ESNamer.Suffix("another-es", "test-secret-mount"), + Namespace: "ns", + }, &secretMountsSecret) + assert.NoError(t, err) + }, + post: func(r ReconcileStackConfigPolicy, recorder record.FakeRecorder) { + // after the reconciliation, settings are empty + settings := r.getSettings(t, k8s.ExtractNamespacedName(orphanSecretFixture)) + assert.Empty(t, settings.State.ClusterSettings.Data) + + var esConfigSecret, secretMountsSecretInEsNamespace corev1.Secret + // after the reconciliation, the config and secret mount secrets do not exist + err := r.Client.Get(context.Background(), types.NamespacedName{ + Name: esv1.StackConfigElasticsearchConfigSecretName("another-es"), + Namespace: "ns", + }, &esConfigSecret) + assert.True(t, apierrors.IsNotFound(err)) + + err = r.Client.Get(context.Background(), types.NamespacedName{ + Name: esv1.ESNamer.Suffix("another-es", "test-secret-mount"), + Namespace: "ns", + }, &secretMountsSecretInEsNamespace) + assert.True(t, apierrors.IsNotFound(err)) + }, + wantErr: false, + }, } for _, tt := range tests { @@ -452,3 +584,16 @@ func getSettingsHash(secret corev1.Secret) (string, error) { } return hash.HashObject(settings.State), nil } + +func assertExpectedESSecretContent(t *testing.T, c client.Client, esName string, expectedSecret corev1.Secret, actualSecretMounts []policyv1alpha1.SecretMount) { + t.Helper() + for _, secretMount := range actualSecretMounts { + var secretMountsSecret corev1.Secret + err := c.Get(context.Background(), types.NamespacedName{ + Namespace: "ns", + Name: esv1.StackConfigAdditionalSecretName(esName, secretMount.SecretName), + }, &secretMountsSecret) + assert.NoError(t, err) + assert.Equal(t, expectedSecret.Data, secretMountsSecret.Data) + } +} diff --git a/pkg/controller/stackconfigpolicy/elasticsearch_config_settings.go b/pkg/controller/stackconfigpolicy/elasticsearch_config_settings.go new file mode 100644 index 00000000000..3fb7a86e89a --- /dev/null +++ b/pkg/controller/stackconfigpolicy/elasticsearch_config_settings.go @@ -0,0 +1,121 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package stackconfigpolicy + +import ( + "context" + "encoding/json" + + commonv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/common/v1" + esv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/elasticsearch/v1" + policyv1alpha1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/stackconfigpolicy/v1alpha1" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/hash" + commonlabels "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/labels" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/common/reconciler" + "github.com/elastic/cloud-on-k8s/v2/pkg/controller/elasticsearch/filesettings" + eslabel "github.com/elastic/cloud-on-k8s/v2/pkg/controller/elasticsearch/label" + "github.com/elastic/cloud-on-k8s/v2/pkg/utils/k8s" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +const ( + ElasticSearchConfigKey = "elasticsearch.json" + SecretsMountKey = "secretMounts.json" + ElasticsearchConfigAndSecretMountsHashAnnotation = "policy.k8s.elastic.co/elasticsearch-config-mounts-hash" //nolint:gosec + SourceSecretAnnotationName = "policy.k8s.elastic.co/source-secret-name" //nolint:gosec +) + +func newElasticsearchConfigSecret(policy policyv1alpha1.StackConfigPolicy, es esv1.Elasticsearch) (corev1.Secret, error) { + secretMountBytes, err := json.Marshal(policy.Spec.Elasticsearch.SecretMounts) + if err != nil { + return corev1.Secret{}, err + } + elasticsearchAndMountsConfigHash := getElasticsearchConfigAndMountsHash(policy.Spec.Elasticsearch.Config, policy.Spec.Elasticsearch.SecretMounts) + var configDataJSONBytes []byte + if policy.Spec.Elasticsearch.Config != nil { + configDataJSONBytes, err = policy.Spec.Elasticsearch.Config.MarshalJSON() + if err != nil { + return corev1.Secret{}, err + } + } + elasticsearchConfigSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: es.Namespace, + Name: esv1.StackConfigElasticsearchConfigSecretName(es.Name), + Labels: eslabel.NewLabels(types.NamespacedName{ + Name: es.Name, + Namespace: es.Namespace, + }), + Annotations: map[string]string{ + ElasticsearchConfigAndSecretMountsHashAnnotation: elasticsearchAndMountsConfigHash, + }, + }, + Data: map[string][]byte{ + ElasticSearchConfigKey: configDataJSONBytes, + SecretsMountKey: secretMountBytes, + }, + } + + // Set Elasticsearch as the soft owner + filesettings.SetSoftOwner(&elasticsearchConfigSecret, policy) + + // Add label to delete secret on deletion of the stack config policy + elasticsearchConfigSecret.Labels[commonlabels.StackConfigPolicyOnDeleteLabelName] = commonlabels.OrphanObjectDeleteOnPolicyDelete + + return elasticsearchConfigSecret, nil +} + +// reconcileSecretMounts creates the secrets in SecretMounts to the respective Elasticsearch namespace where they should be mounted to. +func reconcileSecretMounts(ctx context.Context, c k8s.Client, es esv1.Elasticsearch, policy *policyv1alpha1.StackConfigPolicy) error { + for _, secretMount := range policy.Spec.Elasticsearch.SecretMounts { + additionalSecret := corev1.Secret{} + namespacedName := types.NamespacedName{ + Name: secretMount.SecretName, + Namespace: policy.Namespace, + } + if err := c.Get(ctx, namespacedName, &additionalSecret); err != nil { + return err + } + + // Recreate it in the Elasticsearch namespace, prefix with es name. + secretName := esv1.StackConfigAdditionalSecretName(es.Name, secretMount.SecretName) + expected := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: es.Namespace, + Name: secretName, + Labels: eslabel.NewLabels(types.NamespacedName{ + Name: es.Name, + Namespace: es.Namespace, + }), + Annotations: map[string]string{ + SourceSecretAnnotationName: secretMount.SecretName, + }, + }, + Data: additionalSecret.Data, + } + + // Set stackconfigpolicy as a softowner + filesettings.SetSoftOwner(&expected, *policy) + + // Set the secret to be deleted when the stack config policy is deleted. + expected.Labels[commonlabels.StackConfigPolicyOnDeleteLabelName] = commonlabels.OrphanObjectDeleteOnPolicyDelete + + _, err := reconciler.ReconcileSecret(ctx, c, expected, nil) + if err != nil { + return err + } + } + return nil +} + +func getElasticsearchConfigAndMountsHash(elasticsearchConfig *commonv1.Config, secretMounts []policyv1alpha1.SecretMount) string { + if elasticsearchConfig != nil { + return hash.HashObject([]interface{}{elasticsearchConfig, secretMounts}) + } + return hash.HashObject(secretMounts) +} diff --git a/pkg/controller/stackconfigpolicy/elasticsearch_config_settings_test.go b/pkg/controller/stackconfigpolicy/elasticsearch_config_settings_test.go new file mode 100644 index 00000000000..1cf190b9f3a --- /dev/null +++ b/pkg/controller/stackconfigpolicy/elasticsearch_config_settings_test.go @@ -0,0 +1,140 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +package stackconfigpolicy + +import ( + "context" + "testing" + + "github.com/magiconair/properties/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + esv1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/elasticsearch/v1" + policyv1alpha1 "github.com/elastic/cloud-on-k8s/v2/pkg/apis/stackconfigpolicy/v1alpha1" + "github.com/elastic/cloud-on-k8s/v2/pkg/utils/k8s" +) + +func Test_reconcileSecretMountSecretsESNamespace(t *testing.T) { + type args struct { + client k8s.Client + es esv1.Elasticsearch + policy *policyv1alpha1.StackConfigPolicy + } + + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Create secret mount secrets in ES namespace", + args: args{ + client: k8s.NewFakeClient(getSecretMountSecret(t, "auth-policy-secret", "test-policy-ns", "test-policy", "test-policy-ns", "delete")), + es: esv1.Elasticsearch{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-es", + Namespace: "test-ns", + }, + }, + policy: &policyv1alpha1.StackConfigPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "test-policy-ns", + }, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Elasticsearch: policyv1alpha1.ElasticsearchConfigPolicySpec{ + SecretMounts: []policyv1alpha1.SecretMount{ + { + SecretName: "auth-policy-secret", + MountPath: "/usr/test", + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "Secret mount secret in policy does not exist", + args: args{ + client: k8s.NewFakeClient(), + es: esv1.Elasticsearch{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-es", + Namespace: "test-ns", + }, + }, + policy: &policyv1alpha1.StackConfigPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-policy", + Namespace: "test-policy-ns", + }, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Elasticsearch: policyv1alpha1.ElasticsearchConfigPolicySpec{ + SecretMounts: []policyv1alpha1.SecretMount{ + { + SecretName: "auth-policy-secret", + MountPath: "/usr/test", + }, + }, + }, + }, + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := reconcileSecretMounts(context.TODO(), tt.args.client, tt.args.es, tt.args.policy) + if (err != nil) != tt.wantErr { + t.Errorf("error = %v, wantErr %v", err, tt.wantErr) + return + } + + // Verify secret was created in es namespace + if err == nil { + for _, secretMount := range tt.args.policy.Spec.Elasticsearch.SecretMounts { + expectedSecret := &corev1.Secret{} + expectedNsn := types.NamespacedName{ + Name: esv1.StackConfigAdditionalSecretName(tt.args.es.Name, secretMount.SecretName), + Namespace: "test-ns", + } + err := tt.args.client.Get(context.TODO(), expectedNsn, expectedSecret) + if (err != nil) != tt.wantErr { + t.Errorf("error = %v, wantErr %v", err, tt.wantErr) + return + } + + assert.Equal(t, expectedSecret.Data, getSecretMountSecret(t, esv1.ESNamer.Suffix(tt.args.es.Name, secretMount.SecretName), "test-ns", "test-policy", "test-policy-ns", "delete").Data, "secrets do not match") + } + } + }) + } +} + +func getSecretMountSecret(t *testing.T, name string, namespace string, policyName string, policyNamespace string, orphanObjectOnPolicyDeleteStratergy string) *corev1.Secret { + t.Helper() + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: map[string]string{ + "elasticsearch.k8s.elastic.co/cluster-name": "another-es", + "asset.policy.k8s.elastic.co/on-delete": orphanObjectOnPolicyDeleteStratergy, + "eck.k8s.elastic.co/owner-namespace": policyNamespace, + "eck.k8s.elastic.co/owner-name": policyName, + "eck.k8s.elastic.co/owner-kind": policyv1alpha1.Kind, + }, + }, + Data: map[string][]byte{ + "idfile.txt": []byte("test id file"), + }, + } +}