From d528988b3d9a7a103d87209da53338766d64e687 Mon Sep 17 00:00:00 2001 From: tehbooom Date: Mon, 18 Aug 2025 12:12:01 -0400 Subject: [PATCH 01/14] feat: allow mulitple SCPs per cluster --- config/crds/v1/all-crds.yaml | 71 ++-- .../agent.k8s.elastic.co_agents.yaml | 10 +- .../apm.k8s.elastic.co_apmservers.yaml | 6 +- .../resources/beat.k8s.elastic.co_beats.yaml | 12 +- ...search.k8s.elastic.co_elasticsearches.yaml | 6 +- ...rch.k8s.elastic.co_enterprisesearches.yaml | 6 +- .../kibana.k8s.elastic.co_kibanas.yaml | 12 +- .../logstash.k8s.elastic.co_logstashes.yaml | 9 +- ...aps.k8s.elastic.co_elasticmapsservers.yaml | 3 +- ...cy.k8s.elastic.co_stackconfigpolicies.yaml | 7 + .../eck-operator-crds/templates/all-crds.yaml | 71 ++-- docs/reference/api-reference/main.md | 5 +- pkg/apis/agent/v1alpha1/agent_types.go | 1 - pkg/apis/common/v1/common.go | 3 +- .../v1alpha1/stackconfigpolicy_types.go | 4 + .../filesettings/file_settings.go | 69 +++- .../elasticsearch/filesettings/secret.go | 196 ++++++++++ .../stackconfigpolicy/controller.go | 291 +++++++++++++-- .../stackconfigpolicy/controller_test.go | 28 +- .../elasticsearch_config_settings.go | 222 ++++++++++++ .../kibana_config_settings.go | 148 ++++++++ .../stackconfigpolicy/weight_conflict_test.go | 339 ++++++++++++++++++ 22 files changed, 1326 insertions(+), 193 deletions(-) create mode 100644 pkg/controller/stackconfigpolicy/weight_conflict_test.go diff --git a/config/crds/v1/all-crds.yaml b/config/crds/v1/all-crds.yaml index 43d95fec0e..41e77a1e88 100644 --- a/config/crds/v1/all-crds.yaml +++ b/config/crds/v1/all-crds.yaml @@ -226,8 +226,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -251,7 +250,6 @@ spec: description: |- FleetServerRef is a reference to Fleet Server that this Agent should connect to to obtain it's configuration. Don't set unless `mode` is set to `fleet`. - References to Fleet servers running outside the Kubernetes cluster via the `secretName` attribute are not supported. properties: name: description: Name of an existing Kubernetes object corresponding @@ -264,8 +262,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -716,8 +713,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -1210,8 +1206,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -1661,8 +1656,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -2583,8 +2577,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -2619,8 +2612,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -2665,8 +2657,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -2707,8 +2698,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -2927,8 +2917,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -4325,8 +4314,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -4367,8 +4355,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -6660,8 +6647,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -7239,8 +7225,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -7810,8 +7795,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -7842,8 +7826,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -8307,8 +8290,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -8349,8 +8331,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -9143,8 +9124,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -9196,8 +9176,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -9238,8 +9217,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -10564,6 +10542,13 @@ spec: - secretName type: object type: array + weight: + default: 0 + description: |- + Weight determines the priority of this policy when multiple policies target the same resource. + Lower weight values take precedence (0 is highest priority). Defaults to 0. + format: int32 + type: integer type: object status: properties: diff --git a/config/crds/v1/resources/agent.k8s.elastic.co_agents.yaml b/config/crds/v1/resources/agent.k8s.elastic.co_agents.yaml index 8b18bfb341..6aef4d0804 100644 --- a/config/crds/v1/resources/agent.k8s.elastic.co_agents.yaml +++ b/config/crds/v1/resources/agent.k8s.elastic.co_agents.yaml @@ -16477,8 +16477,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -16502,7 +16501,6 @@ spec: description: |- FleetServerRef is a reference to Fleet Server that this Agent should connect to to obtain it's configuration. Don't set unless `mode` is set to `fleet`. - References to Fleet servers running outside the Kubernetes cluster via the `secretName` attribute are not supported. properties: name: description: Name of an existing Kubernetes object corresponding @@ -16515,8 +16513,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -16967,8 +16964,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource diff --git a/config/crds/v1/resources/apm.k8s.elastic.co_apmservers.yaml b/config/crds/v1/resources/apm.k8s.elastic.co_apmservers.yaml index 06529c2d20..e29e6a1289 100644 --- a/config/crds/v1/resources/apm.k8s.elastic.co_apmservers.yaml +++ b/config/crds/v1/resources/apm.k8s.elastic.co_apmservers.yaml @@ -81,8 +81,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -532,8 +531,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource diff --git a/config/crds/v1/resources/beat.k8s.elastic.co_beats.yaml b/config/crds/v1/resources/beat.k8s.elastic.co_beats.yaml index d4f5713a67..cebc5c508a 100644 --- a/config/crds/v1/resources/beat.k8s.elastic.co_beats.yaml +++ b/config/crds/v1/resources/beat.k8s.elastic.co_beats.yaml @@ -16477,8 +16477,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -16513,8 +16512,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -16559,8 +16557,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -16601,8 +16598,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource diff --git a/config/crds/v1/resources/elasticsearch.k8s.elastic.co_elasticsearches.yaml b/config/crds/v1/resources/elasticsearch.k8s.elastic.co_elasticsearches.yaml index d22f87b3f4..cf14063fd0 100644 --- a/config/crds/v1/resources/elasticsearch.k8s.elastic.co_elasticsearches.yaml +++ b/config/crds/v1/resources/elasticsearch.k8s.elastic.co_elasticsearches.yaml @@ -543,8 +543,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -585,8 +584,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource diff --git a/config/crds/v1/resources/enterprisesearch.k8s.elastic.co_enterprisesearches.yaml b/config/crds/v1/resources/enterprisesearch.k8s.elastic.co_enterprisesearches.yaml index a8ae5aeafc..760880304e 100644 --- a/config/crds/v1/resources/enterprisesearch.k8s.elastic.co_enterprisesearches.yaml +++ b/config/crds/v1/resources/enterprisesearch.k8s.elastic.co_enterprisesearches.yaml @@ -92,8 +92,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -8732,8 +8731,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource diff --git a/config/crds/v1/resources/kibana.k8s.elastic.co_kibanas.yaml b/config/crds/v1/resources/kibana.k8s.elastic.co_kibanas.yaml index 3e6d2d3c15..7ff7d5ef45 100644 --- a/config/crds/v1/resources/kibana.k8s.elastic.co_kibanas.yaml +++ b/config/crds/v1/resources/kibana.k8s.elastic.co_kibanas.yaml @@ -81,8 +81,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -113,8 +112,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -578,8 +576,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -620,8 +617,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource diff --git a/config/crds/v1/resources/logstash.k8s.elastic.co_logstashes.yaml b/config/crds/v1/resources/logstash.k8s.elastic.co_logstashes.yaml index 8bd35fc203..3b183f24a3 100644 --- a/config/crds/v1/resources/logstash.k8s.elastic.co_logstashes.yaml +++ b/config/crds/v1/resources/logstash.k8s.elastic.co_logstashes.yaml @@ -105,8 +105,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -158,8 +157,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -200,8 +198,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource diff --git a/config/crds/v1/resources/maps.k8s.elastic.co_elasticmapsservers.yaml b/config/crds/v1/resources/maps.k8s.elastic.co_elasticmapsservers.yaml index 507aad169b..af4f4d52ca 100644 --- a/config/crds/v1/resources/maps.k8s.elastic.co_elasticmapsservers.yaml +++ b/config/crds/v1/resources/maps.k8s.elastic.co_elasticmapsservers.yaml @@ -93,8 +93,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource diff --git a/config/crds/v1/resources/stackconfigpolicy.k8s.elastic.co_stackconfigpolicies.yaml b/config/crds/v1/resources/stackconfigpolicy.k8s.elastic.co_stackconfigpolicies.yaml index e03b34f23f..26ba3cd041 100644 --- a/config/crds/v1/resources/stackconfigpolicy.k8s.elastic.co_stackconfigpolicies.yaml +++ b/config/crds/v1/resources/stackconfigpolicy.k8s.elastic.co_stackconfigpolicies.yaml @@ -288,6 +288,13 @@ spec: - secretName type: object type: array + weight: + default: 0 + description: |- + Weight determines the priority of this policy when multiple policies target the same resource. + Lower weight values take precedence (0 is highest priority). Defaults to 0. + format: int32 + type: integer type: object status: properties: 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 511dff99d7..4373e77706 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 @@ -233,8 +233,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -258,7 +257,6 @@ spec: description: |- FleetServerRef is a reference to Fleet Server that this Agent should connect to to obtain it's configuration. Don't set unless `mode` is set to `fleet`. - References to Fleet servers running outside the Kubernetes cluster via the `secretName` attribute are not supported. properties: name: description: Name of an existing Kubernetes object corresponding @@ -271,8 +269,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -723,8 +720,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -1224,8 +1220,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -1675,8 +1670,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -2604,8 +2598,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -2640,8 +2633,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -2686,8 +2678,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -2728,8 +2719,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -2955,8 +2945,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -4367,8 +4356,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -4409,8 +4397,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -6709,8 +6696,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -7288,8 +7274,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -7866,8 +7851,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -7898,8 +7882,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -8363,8 +8346,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -8405,8 +8387,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -9206,8 +9187,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -9259,8 +9239,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -9301,8 +9280,7 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. - The referenced secret must contain the following: + Elastic resource not managed by the operator. The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -10634,6 +10612,13 @@ spec: - secretName type: object type: array + weight: + default: 0 + description: |- + Weight determines the priority of this policy when multiple policies target the same resource. + Lower weight values take precedence (0 is highest priority). Defaults to 0. + format: int32 + type: integer type: object status: properties: diff --git a/docs/reference/api-reference/main.md b/docs/reference/api-reference/main.md index c65d614344..67eb56539d 100644 --- a/docs/reference/api-reference/main.md +++ b/docs/reference/api-reference/main.md @@ -92,7 +92,7 @@ AgentSpec defines the desired state of the Agent | *`fleetServerEnabled`* __boolean__ | FleetServerEnabled determines whether this Agent will launch Fleet Server. Don't set unless `mode` is set to `fleet`. | | *`policyID`* __string__ | PolicyID determines into which Agent Policy this Agent will be enrolled.
This field will become mandatory in a future release, default policies are deprecated since 8.1.0. | | *`kibanaRef`* __[ObjectSelector](#objectselector)__ | KibanaRef is a reference to Kibana where Fleet should be set up and this Agent should be enrolled. Don't set
unless `mode` is set to `fleet`. | -| *`fleetServerRef`* __[ObjectSelector](#objectselector)__ | FleetServerRef is a reference to Fleet Server that this Agent should connect to to obtain it's configuration.
Don't set unless `mode` is set to `fleet`.
References to Fleet servers running outside the Kubernetes cluster via the `secretName` attribute are not supported. | +| *`fleetServerRef`* __[ObjectSelector](#objectselector)__ | FleetServerRef is a reference to Fleet Server that this Agent should connect to to obtain it's configuration.
Don't set unless `mode` is set to `fleet`. | ### DaemonSetSpec [#daemonsetspec] @@ -588,7 +588,7 @@ or a Secret describing an external Elastic resource not managed by the operator. | *`namespace`* __string__ | Namespace of the Kubernetes object. If empty, defaults to the current namespace. | | *`name`* __string__ | Name of an existing Kubernetes object corresponding to an Elastic resource managed by ECK. | | *`serviceName`* __string__ | ServiceName is the name of an existing Kubernetes service which is used to make requests to the referenced
object. It has to be in the same namespace as the referenced resource. If left empty, the default HTTP service of
the referenced resource is used. | -| *`secretName`* __string__ | SecretName is the name of an existing Kubernetes secret that contains connection information for associating an
Elastic resource not managed by the operator.
The referenced secret must contain the following:
- `url`: the URL to reach the Elastic resource
- `username`: the username of the user to be authenticated to the Elastic resource
- `password`: the password of the user to be authenticated to the Elastic resource
- `ca.crt`: the CA certificate in PEM format (optional)
- `api-key`: the key to authenticate against the Elastic resource instead of a username and password (supported only for `elasticsearchRefs` in AgentSpec and in BeatSpec)
This field cannot be used in combination with the other fields name, namespace or serviceName. | +| *`secretName`* __string__ | SecretName is the name of an existing Kubernetes secret that contains connection information for associating an
Elastic resource not managed by the operator. The referenced secret must contain the following:
- `url`: the URL to reach the Elastic resource
- `username`: the username of the user to be authenticated to the Elastic resource
- `password`: the password of the user to be authenticated to the Elastic resource
- `ca.crt`: the CA certificate in PEM format (optional)
- `api-key`: the key to authenticate against the Elastic resource instead of a username and password (supported only for `elasticsearchRefs` in AgentSpec and in BeatSpec)
This field cannot be used in combination with the other fields name, namespace or serviceName. | ### PodDisruptionBudgetTemplate [#poddisruptionbudgettemplate] @@ -2066,6 +2066,7 @@ StackConfigPolicy represents a StackConfigPolicy resource in a Kubernetes cluste | Field | Description | | --- | --- | | *`resourceSelector`* __[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#labelselector-v1-meta)__ | | +| *`weight`* __integer__ | Weight determines the priority of this policy when multiple policies target the same resource.
Lower weight values take precedence (0 is highest priority). Defaults to 0. | | *`secureSettings`* __[SecretSource](#secretsource) array__ | Deprecated: SecureSettings only applies to Elasticsearch and is deprecated. It must be set per application instead. | | *`elasticsearch`* __[ElasticsearchConfigPolicySpec](#elasticsearchconfigpolicyspec)__ | | | *`kibana`* __[KibanaConfigPolicySpec](#kibanaconfigpolicyspec)__ | | diff --git a/pkg/apis/agent/v1alpha1/agent_types.go b/pkg/apis/agent/v1alpha1/agent_types.go index f6fb7d4ac0..3d20837f00 100644 --- a/pkg/apis/agent/v1alpha1/agent_types.go +++ b/pkg/apis/agent/v1alpha1/agent_types.go @@ -106,7 +106,6 @@ type AgentSpec struct { // FleetServerRef is a reference to Fleet Server that this Agent should connect to to obtain it's configuration. // Don't set unless `mode` is set to `fleet`. - // References to Fleet servers running outside the Kubernetes cluster via the `secretName` attribute are not supported. // +kubebuilder:validation:Optional FleetServerRef commonv1.ObjectSelector `json:"fleetServerRef,omitempty"` } diff --git a/pkg/apis/common/v1/common.go b/pkg/apis/common/v1/common.go index cecd1a6149..e2fc6bc292 100644 --- a/pkg/apis/common/v1/common.go +++ b/pkg/apis/common/v1/common.go @@ -115,8 +115,7 @@ type ObjectSelector struct { ServiceName string `json:"serviceName,omitempty"` // SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - // Elastic resource not managed by the operator. - // The referenced secret must contain the following: + // Elastic resource not managed by the operator. The referenced secret must contain the following: // - `url`: the URL to reach the Elastic resource // - `username`: the username of the user to be authenticated to the Elastic resource // - `password`: the password of the user to be authenticated to the Elastic resource diff --git a/pkg/apis/stackconfigpolicy/v1alpha1/stackconfigpolicy_types.go b/pkg/apis/stackconfigpolicy/v1alpha1/stackconfigpolicy_types.go index 058f197d23..e4efae1c30 100644 --- a/pkg/apis/stackconfigpolicy/v1alpha1/stackconfigpolicy_types.go +++ b/pkg/apis/stackconfigpolicy/v1alpha1/stackconfigpolicy_types.go @@ -55,6 +55,10 @@ type StackConfigPolicyList struct { type StackConfigPolicySpec struct { ResourceSelector metav1.LabelSelector `json:"resourceSelector,omitempty"` + // Weight determines the priority of this policy when multiple policies target the same resource. + // Lower weight values take precedence (0 is highest priority). Defaults to 0. + // +kubebuilder:default=0 + Weight int32 `json:"weight,omitempty"` // Deprecated: SecureSettings only applies to Elasticsearch and is deprecated. It must be set per application instead. SecureSettings []commonv1.SecretSource `json:"secureSettings,omitempty"` Elasticsearch ElasticsearchConfigPolicySpec `json:"elasticsearch,omitempty"` diff --git a/pkg/controller/elasticsearch/filesettings/file_settings.go b/pkg/controller/elasticsearch/filesettings/file_settings.go index 3bdbab5084..836ff82c31 100644 --- a/pkg/controller/elasticsearch/filesettings/file_settings.go +++ b/pkg/controller/elasticsearch/filesettings/file_settings.go @@ -80,10 +80,44 @@ func newEmptySettingsState() SettingsState { } } +// updateStateFromPolicies merges settings from multiple StackConfigPolicies based on their weights. +// Lower weight policies override higher weight policies for conflicting settings. +func (s *Settings) updateStateFromPolicies(es types.NamespacedName, policies []policyv1alpha1.StackConfigPolicy) error { + if len(policies) == 0 { + return nil + } + + sortedPolicies := make([]policyv1alpha1.StackConfigPolicy, len(policies)) + copy(sortedPolicies, policies) + + // Simple bubble sort by weight (descending order) + for i := 0; i < len(sortedPolicies)-1; i++ { + for j := 0; j < len(sortedPolicies)-i-1; j++ { + if sortedPolicies[j].Spec.Weight < sortedPolicies[j+1].Spec.Weight { + sortedPolicies[j], sortedPolicies[j+1] = sortedPolicies[j+1], sortedPolicies[j] + } + } + } + + // Merge settings from all policies in order of decreasing weight (lower weight = higher priority) + for _, policy := range sortedPolicies { + if err := s.updateState(es, policy); err != nil { + return err + } + } + + return nil +} + // updateState updates the Settings state from a StackConfigPolicy for a given Elasticsearch. func (s *Settings) updateState(es types.NamespacedName, policy policyv1alpha1.StackConfigPolicy) error { p := policy.DeepCopy() // be sure to not mutate the original policy - state := newEmptySettingsState() + + // Initialize state if not already done + if s.State.ClusterSettings == nil { + s.State = newEmptySettingsState() + } + // mutate Snapshot Repositories if p.Spec.Elasticsearch.SnapshotRepositories != nil { for name, untypedDefinition := range p.Spec.Elasticsearch.SnapshotRepositories.Data { @@ -97,34 +131,47 @@ func (s *Settings) updateState(es types.NamespacedName, policy policyv1alpha1.St } p.Spec.Elasticsearch.SnapshotRepositories.Data[name] = repoSettings } - state.SnapshotRepositories = p.Spec.Elasticsearch.SnapshotRepositories + s.mergeConfig(s.State.SnapshotRepositories, p.Spec.Elasticsearch.SnapshotRepositories) } - // just copy other settings + // merge other settings if p.Spec.Elasticsearch.ClusterSettings != nil { - state.ClusterSettings = p.Spec.Elasticsearch.ClusterSettings + s.mergeConfig(s.State.ClusterSettings, p.Spec.Elasticsearch.ClusterSettings) } if p.Spec.Elasticsearch.SnapshotLifecyclePolicies != nil { - state.SLM = p.Spec.Elasticsearch.SnapshotLifecyclePolicies + s.mergeConfig(s.State.SLM, p.Spec.Elasticsearch.SnapshotLifecyclePolicies) } if p.Spec.Elasticsearch.SecurityRoleMappings != nil { - state.RoleMappings = p.Spec.Elasticsearch.SecurityRoleMappings + s.mergeConfig(s.State.RoleMappings, p.Spec.Elasticsearch.SecurityRoleMappings) } if p.Spec.Elasticsearch.IndexLifecyclePolicies != nil { - state.IndexLifecyclePolicies = p.Spec.Elasticsearch.IndexLifecyclePolicies + s.mergeConfig(s.State.IndexLifecyclePolicies, p.Spec.Elasticsearch.IndexLifecyclePolicies) } if p.Spec.Elasticsearch.IngestPipelines != nil { - state.IngestPipelines = p.Spec.Elasticsearch.IngestPipelines + s.mergeConfig(s.State.IngestPipelines, p.Spec.Elasticsearch.IngestPipelines) } if p.Spec.Elasticsearch.IndexTemplates.ComposableIndexTemplates != nil { - state.IndexTemplates.ComposableIndexTemplates = p.Spec.Elasticsearch.IndexTemplates.ComposableIndexTemplates + s.mergeConfig(s.State.IndexTemplates.ComposableIndexTemplates, p.Spec.Elasticsearch.IndexTemplates.ComposableIndexTemplates) } if p.Spec.Elasticsearch.IndexTemplates.ComponentTemplates != nil { - state.IndexTemplates.ComponentTemplates = p.Spec.Elasticsearch.IndexTemplates.ComponentTemplates + s.mergeConfig(s.State.IndexTemplates.ComponentTemplates, p.Spec.Elasticsearch.IndexTemplates.ComponentTemplates) } - s.State = state return nil } +// mergeConfig merges source config into target config, with source taking precedence +func (s *Settings) mergeConfig(target, source *commonv1.Config) { + if source == nil || source.Data == nil { + return + } + if target == nil || target.Data == nil { + target = &commonv1.Config{Data: make(map[string]interface{})} + } + + for key, value := range source.Data { + target.Data[key] = value + } +} + // mutateSnapshotRepositorySettings ensures that a snapshot repository can be used across multiple ES clusters. // The namespace and the Elasticsearch cluster name are injected in the repository settings depending on the type of the repository: // - "azure", "gcs", "s3": if not provided, the `base_path` property is set to `snapshots/-` diff --git a/pkg/controller/elasticsearch/filesettings/secret.go b/pkg/controller/elasticsearch/filesettings/secret.go index 69e9604d4b..e94a01d3dd 100644 --- a/pkg/controller/elasticsearch/filesettings/secret.go +++ b/pkg/controller/elasticsearch/filesettings/secret.go @@ -39,6 +39,85 @@ func NewSettingsSecretWithVersion(es types.NamespacedName, currentSecret *corev1 return newSettingsSecret(newVersion, es, currentSecret, policy, meta) } +// NewSettingsSecretWithVersionFromPolicies returns a new SettingsSecret for a given Elasticsearch from multiple policies. +// Policies are merged based on their weights, with higher weights taking precedence. +func NewSettingsSecretWithVersionFromPolicies(es types.NamespacedName, currentSecret *corev1.Secret, policies []policyv1alpha1.StackConfigPolicy, meta metadata.Metadata) (corev1.Secret, int64, error) { + newVersion := time.Now().UnixNano() + return newSettingsSecretFromPolicies(newVersion, es, currentSecret, policies, meta) +} + +// NewSettingsSecretFromPolicies returns a new SettingsSecret for a given Elasticsearch from multiple StackConfigPolicies. +func newSettingsSecretFromPolicies(version int64, es types.NamespacedName, currentSecret *corev1.Secret, policies []policyv1alpha1.StackConfigPolicy, meta metadata.Metadata) (corev1.Secret, int64, error) { + settings := NewEmptySettings(version) + + // update the settings according to the config policies + if len(policies) > 0 { + err := settings.updateStateFromPolicies(es, policies) + if err != nil { + return corev1.Secret{}, 0, err + } + } + + // do not update version if hash hasn't changed + if currentSecret != nil && !hasChanged(*currentSecret, settings) { + currentVersion, err := extractVersion(*currentSecret) + if err != nil { + return corev1.Secret{}, 0, err + } + + version = currentVersion + settings.Metadata.Version = strconv.FormatInt(currentVersion, 10) + } + + // prepare the SettingsSecret + secretMeta := meta.Merge(metadata.Metadata{ + Annotations: map[string]string{ + commonannotation.SettingsHashAnnotationName: settings.hash(), + }, + }) + settingsBytes, err := json.Marshal(settings) + if err != nil { + return corev1.Secret{}, 0, err + } + settingsSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: es.Namespace, + Name: esv1.FileSettingsSecretName(es.Name), + Labels: secretMeta.Labels, + Annotations: secretMeta.Annotations, + }, + Data: map[string][]byte{ + SettingsSecretKey: settingsBytes, + }, + } + + // Store all policy references in the secret + var policyRefs []PolicyRef + for _, policy := range policies { + policyRefs = append(policyRefs, PolicyRef{ + Name: policy.Name, + Namespace: policy.Namespace, + Weight: policy.Spec.Weight, + }) + } + if err := SetPolicyRefs(settingsSecret, policyRefs); err != nil { + return corev1.Secret{}, 0, err + } + + // Add secure settings from all policies + if err := setSecureSettingsFromPolicies(settingsSecret, policies); err != nil { + return corev1.Secret{}, 0, err + } + + // Add a label to reset secret on deletion of the stack config policy + if settingsSecret.Labels == nil { + settingsSecret.Labels = make(map[string]string) + } + settingsSecret.Labels[commonlabel.StackConfigPolicyOnDeleteLabelName] = commonlabel.OrphanSecretResetOnPolicyDelete + + return *settingsSecret, version, nil +} + // NewSettingsSecret returns a new SettingsSecret for a given Elasticsearch and StackConfigPolicy. func newSettingsSecret(version int64, es types.NamespacedName, currentSecret *corev1.Secret, policy *policyv1alpha1.StackConfigPolicy, meta metadata.Metadata) (corev1.Secret, int64, error) { settings := NewEmptySettings(version) @@ -160,6 +239,35 @@ func setSecureSettings(settingsSecret *corev1.Secret, policy policyv1alpha1.Stac return nil } +// setSecureSettingsFromPolicies sets secure settings from multiple policies into the settings secret +func setSecureSettingsFromPolicies(settingsSecret *corev1.Secret, policies []policyv1alpha1.StackConfigPolicy) error { + var allSecretSources []commonv1.NamespacedSecretSource //nolint:prealloc + + for _, policy := range policies { + // Common secureSettings field, this is mainly there to maintain backwards compatibility + //nolint:staticcheck + for _, src := range policy.Spec.SecureSettings { + allSecretSources = append(allSecretSources, commonv1.NamespacedSecretSource{Namespace: policy.GetNamespace(), SecretName: src.SecretName, Entries: src.Entries}) + } + + // SecureSettings field under Elasticsearch in the StackConfigPolicy + for _, src := range policy.Spec.Elasticsearch.SecureSettings { + allSecretSources = append(allSecretSources, commonv1.NamespacedSecretSource{Namespace: policy.GetNamespace(), SecretName: src.SecretName, Entries: src.Entries}) + } + } + + if len(allSecretSources) == 0 { + return nil + } + + bytes, err := json.Marshal(allSecretSources) + if err != nil { + return err + } + settingsSecret.Annotations[commonannotation.SecureSettingsSecretsAnnotationName] = string(bytes) + return nil +} + // CanBeOwnedBy return true if the Settings Secret can be owned by the given StackConfigPolicy, either because the Secret // belongs to no one or because it already belongs to the given policy. func CanBeOwnedBy(settingsSecret corev1.Secret, policy policyv1alpha1.StackConfigPolicy) (reconciler.SoftOwnerRef, bool) { @@ -173,6 +281,94 @@ func CanBeOwnedBy(settingsSecret corev1.Secret, policy policyv1alpha1.StackConfi return currentOwner, canBeOwned } +// PolicyRef represents a reference to a StackConfigPolicy with its weight +type PolicyRef struct { + Name string + Namespace string + Weight int32 +} + +// GetPolicyRefs extracts all policy references from a secret's annotations +func GetPolicyRefs(secret corev1.Secret) ([]PolicyRef, error) { + if secret.Annotations == nil { + return nil, nil + } + + policiesData, ok := secret.Annotations["stackconfigpolicy.k8s.elastic.co/policies"] + if !ok { + return nil, nil + } + + var policies []PolicyRef + if err := json.Unmarshal([]byte(policiesData), &policies); err != nil { + return nil, err + } + + return policies, nil +} + +// SetPolicyRefs stores policy references in a secret's annotations +func SetPolicyRefs(secret *corev1.Secret, policies []PolicyRef) error { + if secret.Annotations == nil { + secret.Annotations = make(map[string]string) + } + + data, err := json.Marshal(policies) + if err != nil { + return err + } + + secret.Annotations["stackconfigpolicy.k8s.elastic.co/policies"] = string(data) + return nil +} + +// AddOrUpdatePolicyRef adds or updates a policy reference in the secret +func AddOrUpdatePolicyRef(secret *corev1.Secret, policy policyv1alpha1.StackConfigPolicy) error { + policies, err := GetPolicyRefs(*secret) + if err != nil { + return err + } + + policyRef := PolicyRef{ + Name: policy.Name, + Namespace: policy.Namespace, + Weight: policy.Spec.Weight, + } + + // Update existing policy or add new one + found := false + for i, p := range policies { + if p.Name == policy.Name && p.Namespace == policy.Namespace { + policies[i] = policyRef + found = true + break + } + } + + if !found { + policies = append(policies, policyRef) + } + + return SetPolicyRefs(secret, policies) +} + +// RemovePolicyRef removes a policy reference from the secret +func RemovePolicyRef(secret *corev1.Secret, policyName, policyNamespace string) error { + policies, err := GetPolicyRefs(*secret) + if err != nil { + return err + } + + var filtered []PolicyRef + for _, p := range policies { + if !(p.Name == policyName && p.Namespace == policyNamespace) { + filtered = append(filtered, p) + } + } + + return SetPolicyRefs(secret, filtered) +} + // getSecureSettings returns the SecureSettings Secret sources stores in an annotation of the given file settings Secret. func getSecureSettings(settingsSecret corev1.Secret) ([]commonv1.NamespacedSecretSource, error) { rawString, ok := settingsSecret.Annotations[commonannotation.SecureSettingsSecretsAnnotationName] diff --git a/pkg/controller/stackconfigpolicy/controller.go b/pkg/controller/stackconfigpolicy/controller.go index b2cefc8ea5..35a9361dca 100644 --- a/pkg/controller/stackconfigpolicy/controller.go +++ b/pkg/controller/stackconfigpolicy/controller.go @@ -17,6 +17,8 @@ import ( 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/labels" + "k8s.io/apimachinery/pkg/selection" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/client" @@ -233,6 +235,13 @@ func (r *ReconcileStackConfigPolicy) doReconcile(ctx context.Context, policy pol return results.WithError(err), status } + // check for weight conflicts with other policies + if err := r.checkWeightConflicts(ctx, &policy); err != nil { + status.Phase = policyv1alpha1.ConflictPhase + r.recorder.Eventf(&policy, corev1.EventTypeWarning, events.EventReasonValidation, err.Error()) + return results.WithError(err), status + } + // reconcile elasticsearch resources results, status = r.reconcileElasticsearchResources(ctx, policy, status) @@ -250,6 +259,64 @@ func (r *ReconcileStackConfigPolicy) doReconcile(ctx context.Context, policy pol return results, status } +// findPoliciesForElasticsearch finds all StackConfigPolicies that target a given Elasticsearch cluster +func (r *ReconcileStackConfigPolicy) findPoliciesForElasticsearch(ctx context.Context, es esv1.Elasticsearch) ([]policyv1alpha1.StackConfigPolicy, error) { + var allPolicies policyv1alpha1.StackConfigPolicyList + err := r.Client.List(ctx, &allPolicies) + if err != nil { + return nil, err + } + + var matchingPolicies []policyv1alpha1.StackConfigPolicy + for _, policy := range allPolicies.Items { + // Check if policy's resource selector matches this Elasticsearch + selector, err := metav1.LabelSelectorAsSelector(&policy.Spec.ResourceSelector) + if err != nil { + continue // Skip malformed selectors + } + + // Check namespace restrictions + if policy.Namespace != r.params.OperatorNamespace && policy.Namespace != es.Namespace { + continue + } + + if selector.Matches(labels.Set(es.Labels)) { + matchingPolicies = append(matchingPolicies, policy) + } + } + + return matchingPolicies, nil +} + +// findPoliciesForKibana finds all StackConfigPolicies that target a given Kibana instance +func (r *ReconcileStackConfigPolicy) findPoliciesForKibana(ctx context.Context, kibana kibanav1.Kibana) ([]policyv1alpha1.StackConfigPolicy, error) { + var allPolicies policyv1alpha1.StackConfigPolicyList + err := r.Client.List(ctx, &allPolicies) + if err != nil { + return nil, err + } + + var matchingPolicies []policyv1alpha1.StackConfigPolicy + for _, policy := range allPolicies.Items { + // Check if policy's resource selector matches this Kibana + selector, err := metav1.LabelSelectorAsSelector(&policy.Spec.ResourceSelector) + if err != nil { + continue // Skip malformed selectors + } + + // Check namespace restrictions + if policy.Namespace != r.params.OperatorNamespace && policy.Namespace != kibana.Namespace { + continue + } + + if selector.Matches(labels.Set(kibana.Labels)) { + matchingPolicies = append(matchingPolicies, policy) + } + } + + return matchingPolicies, nil +} + func (r *ReconcileStackConfigPolicy) reconcileElasticsearchResources(ctx context.Context, policy policyv1alpha1.StackConfigPolicy, status policyv1alpha1.StackConfigPolicyStatus) (*reconciler.Results, policyv1alpha1.StackConfigPolicyStatus) { defer tracing.Span(&ctx)() log := ulog.FromContext(ctx) @@ -314,23 +381,28 @@ func (r *ReconcileStackConfigPolicy) reconcileElasticsearchResources(ctx context return results.WithError(err), status } - // check that there is no other policy that already owns the Settings Secret - currentOwner, ok := filesettings.CanBeOwnedBy(actualSettingsSecret, policy) - if !ok { - err = fmt.Errorf("conflict: resource Elasticsearch %s/%s already configured by StackConfigpolicy %s/%s", es.Namespace, es.Name, currentOwner.Namespace, currentOwner.Name) - r.recorder.Eventf(&policy, corev1.EventTypeWarning, events.EventReasonUnexpected, err.Error()) - results.WithError(err) - err = status.AddPolicyErrorFor(esNsn, policyv1alpha1.ConflictPhase, err.Error(), policyv1alpha1.ElasticsearchResourceType) - if err != nil { - return results.WithError(err), status - } - continue + // Find all policies that target this Elasticsearch cluster + allPolicies, err := r.findPoliciesForElasticsearch(ctx, es) + if err != nil { + return results.WithError(err), status } // extract the metadata that should be propagated to children meta := metadata.Propagate(&es, metadata.Metadata{Labels: eslabel.NewLabels(k8s.ExtractNamespacedName(&es))}) - // create the expected Settings Secret - expectedSecret, expectedVersion, err := filesettings.NewSettingsSecretWithVersion(esNsn, &actualSettingsSecret, &policy, meta) + + // create the expected Settings Secret from all applicable policies + var expectedSecret corev1.Secret + var expectedVersion int64 + if len(allPolicies) > 1 { + // Multiple policies - use the multi-policy approach + expectedSecret, expectedVersion, err = filesettings.NewSettingsSecretWithVersionFromPolicies(esNsn, &actualSettingsSecret, allPolicies, meta) + } else if len(allPolicies) == 1 { + // Single policy - use the original approach for backward compatibility + expectedSecret, expectedVersion, err = filesettings.NewSettingsSecretWithVersion(esNsn, &actualSettingsSecret, &allPolicies[0], meta) + } else { + // No policies target this resource - skip (shouldn't happen in practice) + continue + } if err != nil { return results.WithError(err), status } @@ -339,8 +411,8 @@ func (r *ReconcileStackConfigPolicy) reconcileElasticsearchResources(ctx context return results.WithError(err), status } - // Copy all the Secrets that are present in spec.elasticsearch.secretMounts - if err := reconcileSecretMounts(ctx, r.Client, es, &policy, meta); err != nil { + // Handle secret mounts and config from all policies + if err := r.reconcileSecretMountsFromPolicies(ctx, es, allPolicies, meta); err != nil { if apierrors.IsNotFound(err) { err = status.AddPolicyErrorFor(esNsn, policyv1alpha1.ErrorPhase, err.Error(), policyv1alpha1.ElasticsearchResourceType) if err != nil { @@ -351,8 +423,8 @@ func (r *ReconcileStackConfigPolicy) reconcileElasticsearchResources(ctx context continue } - // create expected elasticsearch config secret - expectedConfigSecret, err := newElasticsearchConfigSecret(policy, es) + // create expected elasticsearch config secret from all policies + expectedConfigSecret, err := r.newElasticsearchConfigSecretFromPolicies(allPolicies, es) if err != nil { return results.WithError(err), status } @@ -361,8 +433,8 @@ func (r *ReconcileStackConfigPolicy) reconcileElasticsearchResources(ctx context return results.WithError(err), status } - // Check if required Elasticsearch config and secret mounts are applied. - configAndSecretMountsApplied, err := elasticsearchConfigAndSecretMountsApplied(ctx, r.Client, policy, es) + // Check if required Elasticsearch config and secret mounts are applied from all policies. + configAndSecretMountsApplied, err := r.elasticsearchConfigAndSecretMountsAppliedFromPolicies(ctx, allPolicies, es) if err != nil { return results.WithError(err), status } @@ -433,29 +505,37 @@ func (r *ReconcileStackConfigPolicy) reconcileKibanaResources(ctx context.Contex // keep the list of Kibana to be configured kibanaNsn := k8s.ExtractNamespacedName(&kibana) - // check that there is no other policy that already owns the kibana config secret - currentOwner, ok, err := canBeOwned(ctx, r.Client, policy, kibana) + // Find all policies that target this Kibana instance + allPolicies, err := r.findPoliciesForKibana(ctx, kibana) if err != nil { return results.WithError(err), status } - // record error if already owned by another stack config policy - if !ok { - err := fmt.Errorf("conflict: resource Kibana %s/%s already configured by StackConfigpolicy %s/%s", kibana.Namespace, kibana.Name, currentOwner.Namespace, currentOwner.Name) - r.recorder.Eventf(&policy, corev1.EventTypeWarning, events.EventReasonUnexpected, err.Error()) - results.WithError(err) - if err := status.AddPolicyErrorFor(kibanaNsn, policyv1alpha1.ConflictPhase, err.Error(), policyv1alpha1.KibanaResourceType); err != nil { - return results.WithError(err), status + // Check if any policy has Kibana config + hasKibanaConfig := false + for _, p := range allPolicies { + if p.Spec.Kibana.Config != nil { + hasKibanaConfig = true + break } - continue } - // Create the Secret that holds the Kibana configuration. - if policy.Spec.Kibana.Config != nil { - // Only add to configured resources if Kibana config is set. - // This will help clean up the config secret if config gets removed from the stack config policy. + // Create the Secret that holds the Kibana configuration from all policies. + if hasKibanaConfig { + // Only add to configured resources if at least one policy has Kibana config set. configuredResources[kibanaNsn] = kibana - expectedConfigSecret, err := newKibanaConfigSecret(policy, kibana) + + var expectedConfigSecret corev1.Secret + if len(allPolicies) > 1 { + // Multiple policies - use the multi-policy approach + expectedConfigSecret, err = r.newKibanaConfigSecretFromPolicies(allPolicies, kibana) + } else if len(allPolicies) == 1 { + // Single policy - use the original approach for backward compatibility + expectedConfigSecret, err = newKibanaConfigSecret(allPolicies[0], kibana) + } else { + // No policies target this resource - skip (shouldn't happen in practice) + continue + } if err != nil { return results.WithError(err), status } @@ -465,8 +545,15 @@ func (r *ReconcileStackConfigPolicy) reconcileKibanaResources(ctx context.Contex } } - // Check if required Kibana configs are applied. - configApplied, err := kibanaConfigApplied(r.Client, policy, kibana) + // Check if required Kibana configs from all policies are applied. + var configApplied bool + if len(allPolicies) > 1 { + configApplied, err = r.kibanaConfigAppliedFromPolicies(allPolicies, kibana) + } else if len(allPolicies) == 1 { + configApplied, err = kibanaConfigApplied(r.Client, allPolicies[0], kibana) + } else { + configApplied = true // No policies, so nothing to apply + } if err != nil { return results.WithError(err), status } @@ -757,3 +844,135 @@ func (r *ReconcileStackConfigPolicy) addDynamicWatchesOnAdditionalSecretMounts(p func additionalSecretMountsWatcherName(watcher types.NamespacedName) string { return fmt.Sprintf("%s-%s-additional-secret-mounts-watcher", watcher.Name, watcher.Namespace) } + +// checkWeightConflicts validates that no other StackConfigPolicy has the same weight +// and targets overlapping resources +func (r *ReconcileStackConfigPolicy) checkWeightConflicts(ctx context.Context, policy *policyv1alpha1.StackConfigPolicy) error { + var allPolicies policyv1alpha1.StackConfigPolicyList + if err := r.Client.List(ctx, &allPolicies); err != nil { + return fmt.Errorf("failed to list StackConfigPolicies for weight conflict check: %w", err) + } + + policySelector, err := metav1.LabelSelectorAsSelector(&policy.Spec.ResourceSelector) + if err != nil { + return fmt.Errorf("invalid resource selector: %w", err) + } + + // Group policies by weight to detect conflicts more efficiently + policiesByWeight := make(map[int32][]policyv1alpha1.StackConfigPolicy) + for _, otherPolicy := range allPolicies.Items { + // Skip self + if otherPolicy.Namespace == policy.Namespace && otherPolicy.Name == policy.Name { + continue + } + policiesByWeight[otherPolicy.Spec.Weight] = append(policiesByWeight[otherPolicy.Spec.Weight], otherPolicy) + } + + // Check if any policies with the same weight could target overlapping resources + conflictingPolicies := policiesByWeight[policy.Spec.Weight] + if len(conflictingPolicies) == 0 { + return nil // No conflicts + } + + for _, otherPolicy := range conflictingPolicies { + if r.policiesCouldOverlap(ctx, policy, &otherPolicy, policySelector) { + return fmt.Errorf("weight conflict detected: StackConfigPolicy %s/%s has the same weight (%d) and could target overlapping resources. Policies with the same weight must target non-overlapping resources to ensure deterministic behavior", + otherPolicy.Namespace, otherPolicy.Name, policy.Spec.Weight) + } + } + + return nil +} + +// policiesCouldOverlap checks if two policies could potentially target the same resources +func (r *ReconcileStackConfigPolicy) policiesCouldOverlap(ctx context.Context, policy1, policy2 *policyv1alpha1.StackConfigPolicy, policy1Selector labels.Selector) bool { + // Check namespace-based restrictions first + if !r.namespacesCouldOverlap(policy1.Namespace, policy2.Namespace) { + return false + } + + // Parse policy2 selector + policy2Selector, err := metav1.LabelSelectorAsSelector(&policy2.Spec.ResourceSelector) + if err != nil { + // If we can't parse the selector, assume they could overlap to be safe + return true + } + + // Check if selectors could match the same labels + return r.selectorsCouldOverlap(policy1Selector, policy2Selector) +} + +// namespacesCouldOverlap checks if two policies from different namespaces could target the same resources +// Based on the controller logic in reconcileElasticsearchResources and reconcileKibanaResources +func (r *ReconcileStackConfigPolicy) namespacesCouldOverlap(ns1, ns2 string) bool { + // If both policies are in the same namespace, they can overlap + if ns1 == ns2 { + return true + } + + // Check if either policy is in the operator namespace (can target resources in other namespaces) + if ns1 == r.params.OperatorNamespace || ns2 == r.params.OperatorNamespace { + return true + } + + // Policies from different non-operator namespaces cannot overlap + return false +} + +// selectorsCouldOverlap checks if two label selectors could potentially match the same resources +func (r *ReconcileStackConfigPolicy) selectorsCouldOverlap(selector1, selector2 labels.Selector) bool { + // If either selector matches everything, they overlap + if selector1.Empty() || selector2.Empty() { + return true + } + + // Get requirements for both selectors + reqs1, _ := selector1.Requirements() + reqs2, _ := selector2.Requirements() + + // Create maps for easier lookup + equalsReqs1 := make(map[string]map[string]bool) + equalsReqs2 := make(map[string]map[string]bool) + + for _, req := range reqs1 { + if req.Operator() == selection.Equals { + if equalsReqs1[req.Key()] == nil { + equalsReqs1[req.Key()] = make(map[string]bool) + } + for v := range req.Values() { + equalsReqs1[req.Key()][v] = true + } + } + } + + for _, req := range reqs2 { + if req.Operator() == selection.Equals { + if equalsReqs2[req.Key()] == nil { + equalsReqs2[req.Key()] = make(map[string]bool) + } + for v := range req.Values() { + equalsReqs2[req.Key()][v] = true + } + } + } + + // Check for definitely disjoint selectors + for key, values1 := range equalsReqs1 { + if values2, exists := equalsReqs2[key]; exists { + // Both selectors require the same key - check if value sets overlap + hasOverlap := false + for v := range values1 { + if values2[v] { + hasOverlap = true + break + } + } + if !hasOverlap { + return false // Definitely no overlap for this key + } + } + } + + // If we can't prove they're disjoint, assume they could overlap + return true +} diff --git a/pkg/controller/stackconfigpolicy/controller_test.go b/pkg/controller/stackconfigpolicy/controller_test.go index 851c861bed..39b512b5a6 100644 --- a/pkg/controller/stackconfigpolicy/controller_test.go +++ b/pkg/controller/stackconfigpolicy/controller_test.go @@ -364,40 +364,44 @@ func TestReconcileStackConfigPolicy_Reconcile(t *testing.T) { wantRequeueAfter: true, }, { - name: "Reconcile Kibana already owned by another policy", + name: "Reconcile Kibana with multiple policies (multi-policy support)", args: args{ client: k8s.NewFakeClient(&policyFixture, &kibanaFixture, MkKibanaConfigSecret("ns", "another-policy", "ns", "testvalue")), licenseChecker: &license.MockLicenseChecker{EnterpriseEnabled: true}, }, post: func(r ReconcileStackConfigPolicy, recorder record.FakeRecorder) { + // With multi-policy support, no conflict events should be generated events := fetchEvents(&recorder) - assert.ElementsMatch(t, []string{"Warning Unexpected conflict: resource Kibana ns/test-kb already configured by StackConfigpolicy ns/another-policy"}, events) + assert.Empty(t, events) policy := r.getPolicy(t, k8s.ExtractNamespacedName(&policyFixture)) assert.Equal(t, 1, policy.Status.Resources) - assert.Equal(t, 0, policy.Status.Ready) - assert.Equal(t, policyv1alpha1.ConflictPhase, policy.Status.Phase) + // The policy should be applying changes since multi-policy merging is happening + assert.Equal(t, 0, policy.Status.Ready) // Still applying changes + assert.Equal(t, policyv1alpha1.ApplyingChangesPhase, policy.Status.Phase) }, - wantErr: true, - wantRequeueAfter: true, + wantErr: false, + wantRequeueAfter: true, // Should requeue while applying changes }, { - name: "Reconcile Elasticsearch already owned by another policy", + name: "Reconcile Elasticsearch with multiple policies (multi-policy support)", args: args{ client: k8s.NewFakeClient(&policyFixture, &esFixture, conflictingSecretFixture), licenseChecker: &license.MockLicenseChecker{EnterpriseEnabled: true}, }, post: func(r ReconcileStackConfigPolicy, recorder record.FakeRecorder) { + // With multi-policy support, no conflict events should be generated events := fetchEvents(&recorder) - assert.ElementsMatch(t, []string{"Warning Unexpected conflict: resource Elasticsearch ns/test-es already configured by StackConfigpolicy ns/another-policy"}, events) + assert.Empty(t, events) policy := r.getPolicy(t, k8s.ExtractNamespacedName(&policyFixture)) assert.Equal(t, 1, policy.Status.Resources) - assert.Equal(t, 0, policy.Status.Ready) - assert.Equal(t, policyv1alpha1.ConflictPhase, policy.Status.Phase) + // The policy should show an error since Elasticsearch client might not be available in test + assert.Equal(t, 0, policy.Status.Ready) // Error state + assert.Equal(t, policyv1alpha1.ErrorPhase, policy.Status.Phase) // Error due to test limitations }, - wantErr: true, - wantRequeueAfter: true, + wantErr: false, + wantRequeueAfter: true, // Should requeue on errors }, { name: "Elasticsearch cluster in old version without support for file based settings", diff --git a/pkg/controller/stackconfigpolicy/elasticsearch_config_settings.go b/pkg/controller/stackconfigpolicy/elasticsearch_config_settings.go index f190cb637b..120d936871 100644 --- a/pkg/controller/stackconfigpolicy/elasticsearch_config_settings.go +++ b/pkg/controller/stackconfigpolicy/elasticsearch_config_settings.go @@ -144,3 +144,225 @@ func elasticsearchConfigAndSecretMountsApplied(ctx context.Context, c k8s.Client return true, nil } + +// Multi-policy versions of the above functions + +// newElasticsearchConfigSecretFromPolicies creates an Elasticsearch config secret from multiple policies +func (r *ReconcileStackConfigPolicy) newElasticsearchConfigSecretFromPolicies(policies []policyv1alpha1.StackConfigPolicy, es esv1.Elasticsearch) (corev1.Secret, error) { + data := make(map[string][]byte) + var allSecretMounts []policyv1alpha1.SecretMount + var mergedConfig *commonv1.Config + + // Sort policies by weight (descending) so lower weights override higher ones + sortedPolicies := make([]policyv1alpha1.StackConfigPolicy, len(policies)) + copy(sortedPolicies, policies) + for i := 0; i < len(sortedPolicies)-1; i++ { + for j := 0; j < len(sortedPolicies)-i-1; j++ { + if sortedPolicies[j].Spec.Weight < sortedPolicies[j+1].Spec.Weight { + sortedPolicies[j], sortedPolicies[j+1] = sortedPolicies[j+1], sortedPolicies[j] + } + } + } + + // Merge secret mounts from all policies + for _, policy := range sortedPolicies { + allSecretMounts = append(allSecretMounts, policy.Spec.Elasticsearch.SecretMounts...) + + // Merge Elasticsearch configs (lower weight policies override higher ones) + if policy.Spec.Elasticsearch.Config != nil { + if mergedConfig == nil { + mergedConfig = policy.Spec.Elasticsearch.Config.DeepCopy() + } else { + // Merge the config data, with current policy taking precedence + for key, value := range policy.Spec.Elasticsearch.Config.Data { + mergedConfig.Data[key] = value + } + } + } + } + + // Add secret mounts to data if any exist + if len(allSecretMounts) > 0 { + secretMountBytes, err := json.Marshal(allSecretMounts) + if err != nil { + return corev1.Secret{}, err + } + data[SecretsMountKey] = secretMountBytes + } + + // Add merged config to data if it exists + elasticsearchAndMountsConfigHash := getElasticsearchConfigAndMountsHash(mergedConfig, allSecretMounts) + if mergedConfig != nil { + configDataJSONBytes, err := mergedConfig.MarshalJSON() + if err != nil { + return corev1.Secret{}, err + } + data[ElasticSearchConfigKey] = configDataJSONBytes + } + + meta := metadata.Propagate(&es, metadata.Metadata{ + Labels: eslabel.NewLabels(k8s.ExtractNamespacedName(&es)), + Annotations: map[string]string{ + commonannotation.ElasticsearchConfigAndSecretMountsHashAnnotation: elasticsearchAndMountsConfigHash, + }, + }) + elasticsearchConfigSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: es.Namespace, + Name: esv1.StackConfigElasticsearchConfigSecretName(es.Name), + Labels: meta.Labels, + Annotations: meta.Annotations, + }, + Data: data, + } + + // Store all policy references in the secret + var policyRefs []filesettings.PolicyRef + for _, policy := range policies { + policyRefs = append(policyRefs, filesettings.PolicyRef{ + Name: policy.Name, + Namespace: policy.Namespace, + Weight: policy.Spec.Weight, + }) + } + if err := filesettings.SetPolicyRefs(&elasticsearchConfigSecret, policyRefs); err != nil { + return corev1.Secret{}, err + } + + // Add label to delete secret on deletion of stack config policies + elasticsearchConfigSecret.Labels[commonlabels.StackConfigPolicyOnDeleteLabelName] = commonlabels.OrphanSecretDeleteOnPolicyDelete + + return elasticsearchConfigSecret, nil +} + +// reconcileSecretMountsFromPolicies creates secrets from all policies' SecretMounts +func (r *ReconcileStackConfigPolicy) reconcileSecretMountsFromPolicies(ctx context.Context, es esv1.Elasticsearch, policies []policyv1alpha1.StackConfigPolicy, meta metadata.Metadata) error { + // Collect all unique secret mounts from all policies + secretMountMap := make(map[string]policyv1alpha1.SecretMount) // key is secretName to avoid duplicates + for _, policy := range policies { + for _, secretMount := range policy.Spec.Elasticsearch.SecretMounts { + secretMountMap[secretMount.SecretName] = secretMount + } + } + + for _, secretMount := range secretMountMap { + // Find the policy that contains this secret mount (use the first one found for namespace) + var sourcePolicy *policyv1alpha1.StackConfigPolicy + for _, policy := range policies { + for _, mount := range policy.Spec.Elasticsearch.SecretMounts { + if mount.SecretName == secretMount.SecretName { + sourcePolicy = &policy + break + } + } + if sourcePolicy != nil { + break + } + } + + if sourcePolicy == nil { + continue + } + + additionalSecret := corev1.Secret{} + namespacedName := types.NamespacedName{ + Name: secretMount.SecretName, + Namespace: sourcePolicy.Namespace, + } + if err := r.Client.Get(ctx, namespacedName, &additionalSecret); err != nil { + return err + } + + secretMeta := meta.Merge(metadata.Metadata{ + Annotations: map[string]string{ + commonannotation.SourceSecretAnnotationName: secretMount.SecretName, + }, + }) + + // 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: secretMeta.Labels, + Annotations: secretMeta.Annotations, + }, + Data: additionalSecret.Data, + } + + // Store policy references that use this secret mount + var policyRefs []filesettings.PolicyRef + for _, policy := range policies { + for _, mount := range policy.Spec.Elasticsearch.SecretMounts { + if mount.SecretName == secretMount.SecretName { + policyRefs = append(policyRefs, filesettings.PolicyRef{ + Name: policy.Name, + Namespace: policy.Namespace, + Weight: policy.Spec.Weight, + }) + break + } + } + } + if err := filesettings.SetPolicyRefs(&expected, policyRefs); err != nil { + return err + } + + // Set the secret to be deleted when stack config policies are deleted + expected.Labels[commonlabels.StackConfigPolicyOnDeleteLabelName] = commonlabels.OrphanSecretDeleteOnPolicyDelete + + if _, err := reconciler.ReconcileSecret(ctx, r.Client, expected, nil); err != nil { + return err + } + } + return nil +} + +// elasticsearchConfigAndSecretMountsAppliedFromPolicies checks if configs from all policies have been applied +func (r *ReconcileStackConfigPolicy) elasticsearchConfigAndSecretMountsAppliedFromPolicies(ctx context.Context, policies []policyv1alpha1.StackConfigPolicy, es esv1.Elasticsearch) (bool, error) { + // Get Pods for the given Elasticsearch + podList := corev1.PodList{} + if err := r.Client.List(ctx, &podList, client.InNamespace(es.Namespace), client.MatchingLabels{ + eslabel.ClusterNameLabelName: es.Name, + }); err != nil || len(podList.Items) == 0 { + return false, err + } + + // Compute expected hash from merged policies + var allSecretMounts []policyv1alpha1.SecretMount + var mergedConfig *commonv1.Config + + // Sort policies by weight and merge (descending order) + sortedPolicies := make([]policyv1alpha1.StackConfigPolicy, len(policies)) + copy(sortedPolicies, policies) + for i := 0; i < len(sortedPolicies)-1; i++ { + for j := 0; j < len(sortedPolicies)-i-1; j++ { + if sortedPolicies[j].Spec.Weight < sortedPolicies[j+1].Spec.Weight { + sortedPolicies[j], sortedPolicies[j+1] = sortedPolicies[j+1], sortedPolicies[j] + } + } + } + + for _, policy := range sortedPolicies { + allSecretMounts = append(allSecretMounts, policy.Spec.Elasticsearch.SecretMounts...) + if policy.Spec.Elasticsearch.Config != nil { + if mergedConfig == nil { + mergedConfig = policy.Spec.Elasticsearch.Config.DeepCopy() + } else { + for key, value := range policy.Spec.Elasticsearch.Config.Data { + mergedConfig.Data[key] = value + } + } + } + } + + expectedHash := getElasticsearchConfigAndMountsHash(mergedConfig, allSecretMounts) + for _, esPod := range podList.Items { + if esPod.Annotations[commonannotation.ElasticsearchConfigAndSecretMountsHashAnnotation] != expectedHash { + return false, nil + } + } + + return true, nil +} diff --git a/pkg/controller/stackconfigpolicy/kibana_config_settings.go b/pkg/controller/stackconfigpolicy/kibana_config_settings.go index b740661052..24d079058f 100644 --- a/pkg/controller/stackconfigpolicy/kibana_config_settings.go +++ b/pkg/controller/stackconfigpolicy/kibana_config_settings.go @@ -144,3 +144,151 @@ func setKibanaSecureSettings(settingsSecret *corev1.Secret, policy policyv1alpha settingsSecret.Annotations[commonannotation.SecureSettingsSecretsAnnotationName] = string(bytes) return nil } + +// Multi-policy versions of Kibana functions + +// newKibanaConfigSecretFromPolicies creates a Kibana config secret from multiple policies +func (r *ReconcileStackConfigPolicy) newKibanaConfigSecretFromPolicies(policies []policyv1alpha1.StackConfigPolicy, kibana kibanav1.Kibana) (corev1.Secret, error) { + var mergedConfig *commonv1.Config + + // Sort policies by weight (descending) so lower weights override higher ones + sortedPolicies := make([]policyv1alpha1.StackConfigPolicy, len(policies)) + copy(sortedPolicies, policies) + for i := 0; i < len(sortedPolicies)-1; i++ { + for j := 0; j < len(sortedPolicies)-i-1; j++ { + if sortedPolicies[j].Spec.Weight < sortedPolicies[j+1].Spec.Weight { + sortedPolicies[j], sortedPolicies[j+1] = sortedPolicies[j+1], sortedPolicies[j] + } + } + } + + // Merge Kibana configs (lower weight policies override higher ones) + for _, policy := range sortedPolicies { + if policy.Spec.Kibana.Config != nil { + if mergedConfig == nil { + mergedConfig = policy.Spec.Kibana.Config.DeepCopy() + } else { + // Merge the config data, with current policy taking precedence + for key, value := range policy.Spec.Kibana.Config.Data { + mergedConfig.Data[key] = value + } + } + } + } + + kibanaConfigHash := getKibanaConfigHash(mergedConfig) + configDataJSONBytes := []byte("") + var err error + if mergedConfig != nil { + if configDataJSONBytes, err = mergedConfig.MarshalJSON(); err != nil { + return corev1.Secret{}, err + } + } + + meta := metadata.Propagate(&kibana, metadata.Metadata{ + Labels: kblabel.NewLabels(k8s.ExtractNamespacedName(&kibana)), + Annotations: map[string]string{ + commonannotation.KibanaConfigHashAnnotation: kibanaConfigHash, + }, + }) + kibanaConfigSecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: kibana.Namespace, + Name: GetPolicyConfigSecretName(kibana.Name), + Labels: meta.Labels, + Annotations: meta.Annotations, + }, + Data: map[string][]byte{ + KibanaConfigKey: configDataJSONBytes, + }, + } + + // Store all policy references in the secret + var policyRefs []filesettings.PolicyRef + for _, policy := range policies { + policyRefs = append(policyRefs, filesettings.PolicyRef{ + Name: policy.Name, + Namespace: policy.Namespace, + Weight: policy.Spec.Weight, + }) + } + if err := filesettings.SetPolicyRefs(&kibanaConfigSecret, policyRefs); err != nil { + return corev1.Secret{}, err + } + + // Add label to delete secret on deletion of stack config policies + kibanaConfigSecret.Labels[commonlabels.StackConfigPolicyOnDeleteLabelName] = commonlabels.OrphanSecretDeleteOnPolicyDelete + + // Add SecureSettings from all policies as annotation + if err = r.setKibanaSecureSettingsFromPolicies(&kibanaConfigSecret, policies); err != nil { + return kibanaConfigSecret, err + } + + return kibanaConfigSecret, nil +} + +// kibanaConfigAppliedFromPolicies checks if configs from all policies have been applied to Kibana +func (r *ReconcileStackConfigPolicy) kibanaConfigAppliedFromPolicies(policies []policyv1alpha1.StackConfigPolicy, kb kibanav1.Kibana) (bool, error) { + existingKibanaPods, err := k8s.PodsMatchingLabels(r.Client, kb.Namespace, map[string]string{"kibana.k8s.elastic.co/name": kb.Name}) + if err != nil || len(existingKibanaPods) == 0 { + return false, err + } + + // Compute expected hash from merged policies + var mergedConfig *commonv1.Config + + // Sort policies by weight and merge (descending order) + sortedPolicies := make([]policyv1alpha1.StackConfigPolicy, len(policies)) + copy(sortedPolicies, policies) + for i := 0; i < len(sortedPolicies)-1; i++ { + for j := 0; j < len(sortedPolicies)-i-1; j++ { + if sortedPolicies[j].Spec.Weight < sortedPolicies[j+1].Spec.Weight { + sortedPolicies[j], sortedPolicies[j+1] = sortedPolicies[j+1], sortedPolicies[j] + } + } + } + + for _, policy := range sortedPolicies { + if policy.Spec.Kibana.Config != nil { + if mergedConfig == nil { + mergedConfig = policy.Spec.Kibana.Config.DeepCopy() + } else { + for key, value := range policy.Spec.Kibana.Config.Data { + mergedConfig.Data[key] = value + } + } + } + } + + expectedHash := getKibanaConfigHash(mergedConfig) + for _, kbPod := range existingKibanaPods { + if kbPod.Annotations[commonannotation.KibanaConfigHashAnnotation] != expectedHash { + return false, nil + } + } + + return true, nil +} + +// setKibanaSecureSettingsFromPolicies stores secure settings from multiple policies +func (r *ReconcileStackConfigPolicy) setKibanaSecureSettingsFromPolicies(settingsSecret *corev1.Secret, policies []policyv1alpha1.StackConfigPolicy) error { + var allSecretSources []commonv1.NamespacedSecretSource //nolint:prealloc + + for _, policy := range policies { + // SecureSettings field under Kibana in the StackConfigPolicy + for _, src := range policy.Spec.Kibana.SecureSettings { + allSecretSources = append(allSecretSources, commonv1.NamespacedSecretSource{Namespace: policy.GetNamespace(), SecretName: src.SecretName, Entries: src.Entries}) + } + } + + if len(allSecretSources) == 0 { + return nil + } + + bytes, err := json.Marshal(allSecretSources) + if err != nil { + return err + } + settingsSecret.Annotations[commonannotation.SecureSettingsSecretsAnnotationName] = string(bytes) + return nil +} diff --git a/pkg/controller/stackconfigpolicy/weight_conflict_test.go b/pkg/controller/stackconfigpolicy/weight_conflict_test.go new file mode 100644 index 0000000000..920e3df1e7 --- /dev/null +++ b/pkg/controller/stackconfigpolicy/weight_conflict_test.go @@ -0,0 +1,339 @@ +// 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/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + policyv1alpha1 "github.com/elastic/cloud-on-k8s/v3/pkg/apis/stackconfigpolicy/v1alpha1" + "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/license" + "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/operator" + "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/watches" +) + +func TestCheckWeightConflicts(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, policyv1alpha1.AddToScheme(scheme)) + + tests := []struct { + name string + currentPolicy *policyv1alpha1.StackConfigPolicy + existingPolicies []policyv1alpha1.StackConfigPolicy + operatorNamespace string + expectError bool + errorContains string + }{ + { + name: "no conflicts - different weights", + currentPolicy: &policyv1alpha1.StackConfigPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "policy1", Namespace: "default"}, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 10, + ResourceSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "elasticsearch"}, + }, + }, + }, + existingPolicies: []policyv1alpha1.StackConfigPolicy{ + { + ObjectMeta: metav1.ObjectMeta{Name: "policy2", Namespace: "default"}, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 20, + ResourceSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "elasticsearch"}, + }, + }, + }, + }, + operatorNamespace: "elastic-system", + expectError: false, + }, + { + name: "conflict - same weight, overlapping selectors", + currentPolicy: &policyv1alpha1.StackConfigPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "policy1", Namespace: "default"}, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 10, + ResourceSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "elasticsearch"}, + }, + }, + }, + existingPolicies: []policyv1alpha1.StackConfigPolicy{ + { + ObjectMeta: metav1.ObjectMeta{Name: "policy2", Namespace: "default"}, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 10, + ResourceSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "elasticsearch"}, + }, + }, + }, + }, + operatorNamespace: "elastic-system", + expectError: true, + errorContains: "weight conflict detected", + }, + { + name: "no conflict - same weight, disjoint selectors", + currentPolicy: &policyv1alpha1.StackConfigPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "policy1", Namespace: "default"}, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 10, + ResourceSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "elasticsearch"}, + }, + }, + }, + existingPolicies: []policyv1alpha1.StackConfigPolicy{ + { + ObjectMeta: metav1.ObjectMeta{Name: "policy2", Namespace: "default"}, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 10, + ResourceSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "kibana"}, + }, + }, + }, + }, + operatorNamespace: "elastic-system", + expectError: false, + }, + { + name: "no conflict - same weight, different namespaces (non-operator)", + currentPolicy: &policyv1alpha1.StackConfigPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "policy1", Namespace: "namespace1"}, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 10, + ResourceSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "elasticsearch"}, + }, + }, + }, + existingPolicies: []policyv1alpha1.StackConfigPolicy{ + { + ObjectMeta: metav1.ObjectMeta{Name: "policy2", Namespace: "namespace2"}, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 10, + ResourceSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "elasticsearch"}, + }, + }, + }, + }, + operatorNamespace: "elastic-system", + expectError: false, + }, + { + name: "conflict - same weight, operator namespace policy", + currentPolicy: &policyv1alpha1.StackConfigPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "policy1", Namespace: "elastic-system"}, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 10, + ResourceSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "elasticsearch"}, + }, + }, + }, + existingPolicies: []policyv1alpha1.StackConfigPolicy{ + { + ObjectMeta: metav1.ObjectMeta{Name: "policy2", Namespace: "default"}, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 10, + ResourceSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "elasticsearch"}, + }, + }, + }, + }, + operatorNamespace: "elastic-system", + expectError: true, + errorContains: "weight conflict detected", + }, + { + name: "no conflict - empty selectors but different namespaces", + currentPolicy: &policyv1alpha1.StackConfigPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "policy1", Namespace: "namespace1"}, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 10, + ResourceSelector: metav1.LabelSelector{}, + }, + }, + existingPolicies: []policyv1alpha1.StackConfigPolicy{ + { + ObjectMeta: metav1.ObjectMeta{Name: "policy2", Namespace: "namespace2"}, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 10, + ResourceSelector: metav1.LabelSelector{}, + }, + }, + }, + operatorNamespace: "elastic-system", + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + objs := make([]client.Object, len(tt.existingPolicies)) + for i := range tt.existingPolicies { + objs[i] = &tt.existingPolicies[i] + } + + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(objs...). + Build() + + r := &ReconcileStackConfigPolicy{ + Client: fakeClient, + params: operator.Parameters{ + OperatorNamespace: tt.operatorNamespace, + }, + recorder: record.NewFakeRecorder(10), + licenseChecker: &license.MockLicenseChecker{EnterpriseEnabled: true}, + dynamicWatches: watches.NewDynamicWatches(), + } + + err := r.checkWeightConflicts(context.Background(), tt.currentPolicy) + + if tt.expectError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestSelectorsCouldOverlap(t *testing.T) { + scheme := runtime.NewScheme() + require.NoError(t, policyv1alpha1.AddToScheme(scheme)) + + r := &ReconcileStackConfigPolicy{} + + tests := []struct { + name string + selector1 metav1.LabelSelector + selector2 metav1.LabelSelector + expected bool + }{ + { + name: "both empty selectors", + selector1: metav1.LabelSelector{}, + selector2: metav1.LabelSelector{}, + expected: true, + }, + { + name: "one empty selector", + selector1: metav1.LabelSelector{}, + selector2: metav1.LabelSelector{MatchLabels: map[string]string{"app": "test"}}, + expected: true, + }, + { + name: "same labels", + selector1: metav1.LabelSelector{MatchLabels: map[string]string{"app": "elasticsearch"}}, + selector2: metav1.LabelSelector{MatchLabels: map[string]string{"app": "elasticsearch"}}, + expected: true, + }, + { + name: "different labels", + selector1: metav1.LabelSelector{MatchLabels: map[string]string{"app": "elasticsearch"}}, + selector2: metav1.LabelSelector{MatchLabels: map[string]string{"app": "kibana"}}, + expected: false, + }, + { + name: "overlapping labels", + selector1: metav1.LabelSelector{MatchLabels: map[string]string{"app": "elasticsearch", "env": "prod"}}, + selector2: metav1.LabelSelector{MatchLabels: map[string]string{"app": "elasticsearch", "version": "8.0"}}, + expected: true, + }, + { + name: "completely disjoint labels", + selector1: metav1.LabelSelector{MatchLabels: map[string]string{"app": "elasticsearch", "env": "prod"}}, + selector2: metav1.LabelSelector{MatchLabels: map[string]string{"app": "kibana", "env": "test"}}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + selector1, err := metav1.LabelSelectorAsSelector(&tt.selector1) + require.NoError(t, err) + + selector2, err := metav1.LabelSelectorAsSelector(&tt.selector2) + require.NoError(t, err) + + result := r.selectorsCouldOverlap(selector1, selector2) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestNamespacesCouldOverlap(t *testing.T) { + r := &ReconcileStackConfigPolicy{ + params: operator.Parameters{ + OperatorNamespace: "elastic-system", + }, + } + + tests := []struct { + name string + ns1 string + ns2 string + expected bool + }{ + { + name: "same namespace", + ns1: "default", + ns2: "default", + expected: true, + }, + { + name: "different non-operator namespaces", + ns1: "namespace1", + ns2: "namespace2", + expected: false, + }, + { + name: "one is operator namespace", + ns1: "elastic-system", + ns2: "default", + expected: true, + }, + { + name: "other is operator namespace", + ns1: "default", + ns2: "elastic-system", + expected: true, + }, + { + name: "both are operator namespace", + ns1: "elastic-system", + ns2: "elastic-system", + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := r.namespacesCouldOverlap(tt.ns1, tt.ns2) + assert.Equal(t, tt.expected, result) + }) + } +} + From e8a95dcb4eb88df5ba63613b55b9fccbe1b31fb3 Mon Sep 17 00:00:00 2001 From: tehbooom Date: Mon, 18 Aug 2025 13:49:06 -0400 Subject: [PATCH 02/14] fix: same weight different resources --- .../filesettings/file_settings.go | 14 ++ .../stackconfigpolicy/controller.go | 138 +++++++++++++++++- .../stackconfigpolicy/weight_conflict_test.go | 104 +++++++++++++ 3 files changed, 252 insertions(+), 4 deletions(-) diff --git a/pkg/controller/elasticsearch/filesettings/file_settings.go b/pkg/controller/elasticsearch/filesettings/file_settings.go index 836ff82c31..74b03ee18f 100644 --- a/pkg/controller/elasticsearch/filesettings/file_settings.go +++ b/pkg/controller/elasticsearch/filesettings/file_settings.go @@ -159,6 +159,7 @@ func (s *Settings) updateState(es types.NamespacedName, policy policyv1alpha1.St } // mergeConfig merges source config into target config, with source taking precedence +// For map-type values (like snapshot repositories), individual entries are merged rather than replaced func (s *Settings) mergeConfig(target, source *commonv1.Config) { if source == nil || source.Data == nil { return @@ -168,6 +169,19 @@ func (s *Settings) mergeConfig(target, source *commonv1.Config) { } for key, value := range source.Data { + // Check if both target and source values are maps (like snapshot repositories) + if targetValue, exists := target.Data[key]; exists { + if targetMap, targetIsMap := targetValue.(map[string]interface{}); targetIsMap { + if sourceMap, sourceIsMap := value.(map[string]interface{}); sourceIsMap { + // Deep merge maps - source entries take precedence + for subKey, subValue := range sourceMap { + targetMap[subKey] = subValue + } + continue + } + } + } + // For non-map values or if target doesn't exist, replace entirely target.Data[key] = value } } diff --git a/pkg/controller/stackconfigpolicy/controller.go b/pkg/controller/stackconfigpolicy/controller.go index 35a9361dca..2609b0bc51 100644 --- a/pkg/controller/stackconfigpolicy/controller.go +++ b/pkg/controller/stackconfigpolicy/controller.go @@ -846,7 +846,7 @@ func additionalSecretMountsWatcherName(watcher types.NamespacedName) string { } // checkWeightConflicts validates that no other StackConfigPolicy has the same weight -// and targets overlapping resources +// and would create conflicting configuration for the same resources func (r *ReconcileStackConfigPolicy) checkWeightConflicts(ctx context.Context, policy *policyv1alpha1.StackConfigPolicy) error { var allPolicies policyv1alpha1.StackConfigPolicyList if err := r.Client.List(ctx, &allPolicies); err != nil { @@ -868,7 +868,7 @@ func (r *ReconcileStackConfigPolicy) checkWeightConflicts(ctx context.Context, p policiesByWeight[otherPolicy.Spec.Weight] = append(policiesByWeight[otherPolicy.Spec.Weight], otherPolicy) } - // Check if any policies with the same weight could target overlapping resources + // Check if any policies with the same weight could target overlapping resources and have conflicting settings conflictingPolicies := policiesByWeight[policy.Spec.Weight] if len(conflictingPolicies) == 0 { return nil // No conflicts @@ -876,14 +876,144 @@ func (r *ReconcileStackConfigPolicy) checkWeightConflicts(ctx context.Context, p for _, otherPolicy := range conflictingPolicies { if r.policiesCouldOverlap(ctx, policy, &otherPolicy, policySelector) { - return fmt.Errorf("weight conflict detected: StackConfigPolicy %s/%s has the same weight (%d) and could target overlapping resources. Policies with the same weight must target non-overlapping resources to ensure deterministic behavior", - otherPolicy.Namespace, otherPolicy.Name, policy.Spec.Weight) + // Check if the policies have conflicting settings + if r.policiesHaveConflictingSettings(policy, &otherPolicy) { + return fmt.Errorf("weight conflict detected: StackConfigPolicy %s/%s has the same weight (%d) and would overwrite conflicting settings. Policies with the same weight that target overlapping resources must configure different, non-conflicting settings", + otherPolicy.Namespace, otherPolicy.Name, policy.Spec.Weight) + } } } return nil } +// policiesHaveConflictingSettings checks if two policies would configure conflicting settings +// that would overwrite each other. Returns true if both policies configure the same setting keys, +// or if both policies have completely empty configurations (to maintain existing behavior). +func (r *ReconcileStackConfigPolicy) policiesHaveConflictingSettings(policy1, policy2 *policyv1alpha1.StackConfigPolicy) bool { + // Check if both policies are essentially empty (no meaningful configuration) + if r.policyIsEmpty(policy1) && r.policyIsEmpty(policy2) { + return true // Both empty policies would conflict in the same namespace/selectors + } + + // Check Elasticsearch settings for conflicts + if r.elasticsearchSettingsConflict(policy1, policy2) { + return true + } + + // Check Kibana settings for conflicts + if r.kibanaSettingsConflict(policy1, policy2) { + return true + } + + return false +} + +// policyIsEmpty checks if a policy has no meaningful configuration +func (r *ReconcileStackConfigPolicy) policyIsEmpty(policy *policyv1alpha1.StackConfigPolicy) bool { + es := &policy.Spec.Elasticsearch + kb := &policy.Spec.Kibana + + // Check if Elasticsearch settings are empty + esEmpty := (es.ClusterSettings == nil || len(es.ClusterSettings.Data) == 0) && + (es.SnapshotRepositories == nil || len(es.SnapshotRepositories.Data) == 0) && + (es.SnapshotLifecyclePolicies == nil || len(es.SnapshotLifecyclePolicies.Data) == 0) && + (es.SecurityRoleMappings == nil || len(es.SecurityRoleMappings.Data) == 0) && + (es.IndexLifecyclePolicies == nil || len(es.IndexLifecyclePolicies.Data) == 0) && + (es.IngestPipelines == nil || len(es.IngestPipelines.Data) == 0) && + (es.IndexTemplates.ComponentTemplates == nil || len(es.IndexTemplates.ComponentTemplates.Data) == 0) && + (es.IndexTemplates.ComposableIndexTemplates == nil || len(es.IndexTemplates.ComposableIndexTemplates.Data) == 0) && + (es.Config == nil || len(es.Config.Data) == 0) && + len(es.SecretMounts) == 0 + + // Check if Kibana settings are empty + kbEmpty := (kb.Config == nil || len(kb.Config.Data) == 0) + + return esEmpty && kbEmpty +} + +// elasticsearchSettingsConflict checks if two policies have conflicting Elasticsearch settings +func (r *ReconcileStackConfigPolicy) elasticsearchSettingsConflict(policy1, policy2 *policyv1alpha1.StackConfigPolicy) bool { + es1 := &policy1.Spec.Elasticsearch + es2 := &policy2.Spec.Elasticsearch + + // Check each type of setting for key conflicts + if r.configsConflict(es1.ClusterSettings, es2.ClusterSettings) { + return true + } + if r.configsConflict(es1.SnapshotRepositories, es2.SnapshotRepositories) { + return true + } + if r.configsConflict(es1.SnapshotLifecyclePolicies, es2.SnapshotLifecyclePolicies) { + return true + } + if r.configsConflict(es1.SecurityRoleMappings, es2.SecurityRoleMappings) { + return true + } + if r.configsConflict(es1.IndexLifecyclePolicies, es2.IndexLifecyclePolicies) { + return true + } + if r.configsConflict(es1.IngestPipelines, es2.IngestPipelines) { + return true + } + if r.configsConflict(es1.IndexTemplates.ComponentTemplates, es2.IndexTemplates.ComponentTemplates) { + return true + } + if r.configsConflict(es1.IndexTemplates.ComposableIndexTemplates, es2.IndexTemplates.ComposableIndexTemplates) { + return true + } + if r.configsConflict(es1.Config, es2.Config) { + return true + } + + // Check secret mounts for path conflicts + return r.secretMountsConflict(es1.SecretMounts, es2.SecretMounts) +} + +// kibanaSettingsConflict checks if two policies have conflicting Kibana settings +func (r *ReconcileStackConfigPolicy) kibanaSettingsConflict(policy1, policy2 *policyv1alpha1.StackConfigPolicy) bool { + kb1 := &policy1.Spec.Kibana + kb2 := &policy2.Spec.Kibana + + return r.configsConflict(kb1.Config, kb2.Config) +} + +// configsConflict checks if two Config objects have overlapping keys +func (r *ReconcileStackConfigPolicy) configsConflict(config1, config2 *commonv1.Config) bool { + if config1 == nil || config2 == nil || config1.Data == nil || config2.Data == nil { + return false + } + + // Check if there are any common keys + for key := range config1.Data { + if _, exists := config2.Data[key]; exists { + return true + } + } + + return false +} + +// secretMountsConflict checks if two sets of secret mounts have overlapping mount paths +func (r *ReconcileStackConfigPolicy) secretMountsConflict(mounts1, mounts2 []policyv1alpha1.SecretMount) bool { + if len(mounts1) == 0 || len(mounts2) == 0 { + return false + } + + paths1 := make(map[string]bool) + for _, mount := range mounts1 { + paths1[mount.MountPath] = true + } + + for _, mount := range mounts2 { + if paths1[mount.MountPath] { + return true + } + } + + return false +} + // policiesCouldOverlap checks if two policies could potentially target the same resources func (r *ReconcileStackConfigPolicy) policiesCouldOverlap(ctx context.Context, policy1, policy2 *policyv1alpha1.StackConfigPolicy, policy1Selector labels.Selector) bool { // Check namespace-based restrictions first diff --git a/pkg/controller/stackconfigpolicy/weight_conflict_test.go b/pkg/controller/stackconfigpolicy/weight_conflict_test.go index 920e3df1e7..523ce59914 100644 --- a/pkg/controller/stackconfigpolicy/weight_conflict_test.go +++ b/pkg/controller/stackconfigpolicy/weight_conflict_test.go @@ -16,6 +16,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + commonv1 "github.com/elastic/cloud-on-k8s/v3/pkg/apis/common/v1" policyv1alpha1 "github.com/elastic/cloud-on-k8s/v3/pkg/apis/stackconfigpolicy/v1alpha1" "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/license" "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/operator" @@ -182,6 +183,109 @@ func TestCheckWeightConflicts(t *testing.T) { operatorNamespace: "elastic-system", expectError: false, }, + { + name: "no conflict - same weight, same selectors, but different snapshot repositories", + currentPolicy: &policyv1alpha1.StackConfigPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "policy1", Namespace: "default"}, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 10, + ResourceSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "elasticsearch"}, + }, + Elasticsearch: policyv1alpha1.ElasticsearchConfigPolicySpec{ + SnapshotRepositories: &commonv1.Config{ + Data: map[string]interface{}{ + "policy-1-backups": map[string]interface{}{ + "type": "s3", + "settings": map[string]interface{}{ + "bucket": "policy-1-backups", + "region": "us-west-2", + }, + }, + }, + }, + }, + }, + }, + existingPolicies: []policyv1alpha1.StackConfigPolicy{ + { + ObjectMeta: metav1.ObjectMeta{Name: "policy2", Namespace: "default"}, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 10, + ResourceSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "elasticsearch"}, + }, + Elasticsearch: policyv1alpha1.ElasticsearchConfigPolicySpec{ + SnapshotRepositories: &commonv1.Config{ + Data: map[string]interface{}{ + "policy-2-backups": map[string]interface{}{ + "type": "s3", + "settings": map[string]interface{}{ + "bucket": "policy-2-backups", + "region": "us-east-1", + }, + }, + }, + }, + }, + }, + }, + }, + operatorNamespace: "elastic-system", + expectError: false, + }, + { + name: "conflict - same weight, same selectors, conflicting snapshot repositories", + currentPolicy: &policyv1alpha1.StackConfigPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: "policy1", Namespace: "default"}, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 10, + ResourceSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "elasticsearch"}, + }, + Elasticsearch: policyv1alpha1.ElasticsearchConfigPolicySpec{ + SnapshotRepositories: &commonv1.Config{ + Data: map[string]interface{}{ + "shared-backups": map[string]interface{}{ + "type": "s3", + "settings": map[string]interface{}{ + "bucket": "policy-1-backups", + "region": "us-west-2", + }, + }, + }, + }, + }, + }, + }, + existingPolicies: []policyv1alpha1.StackConfigPolicy{ + { + ObjectMeta: metav1.ObjectMeta{Name: "policy2", Namespace: "default"}, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 10, + ResourceSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "elasticsearch"}, + }, + Elasticsearch: policyv1alpha1.ElasticsearchConfigPolicySpec{ + SnapshotRepositories: &commonv1.Config{ + Data: map[string]interface{}{ + "shared-backups": map[string]interface{}{ + "type": "s3", + "settings": map[string]interface{}{ + "bucket": "policy-2-backups", + "region": "us-east-1", + }, + }, + }, + }, + }, + }, + }, + }, + operatorNamespace: "elastic-system", + expectError: true, + errorContains: "weight conflict detected", + }, } for _, tt := range tests { From 706aca4a7452196f987bf24419637790517a4d7f Mon Sep 17 00:00:00 2001 From: tehbooom Date: Mon, 18 Aug 2025 14:37:16 -0400 Subject: [PATCH 03/14] fix: revert comments back to original --- config/crds/v1/all-crds.yaml | 64 +++++++++++++------ .../agent.k8s.elastic.co_agents.yaml | 10 ++- .../apm.k8s.elastic.co_apmservers.yaml | 6 +- .../resources/beat.k8s.elastic.co_beats.yaml | 12 ++-- ...search.k8s.elastic.co_elasticsearches.yaml | 6 +- ...rch.k8s.elastic.co_enterprisesearches.yaml | 6 +- .../kibana.k8s.elastic.co_kibanas.yaml | 12 ++-- .../logstash.k8s.elastic.co_logstashes.yaml | 9 ++- ...aps.k8s.elastic.co_elasticmapsservers.yaml | 3 +- .../eck-operator-crds/templates/all-crds.yaml | 64 +++++++++++++------ docs/reference/api-reference/main.md | 4 +- pkg/apis/agent/v1alpha1/agent_types.go | 1 + pkg/apis/common/v1/common.go | 3 +- 13 files changed, 134 insertions(+), 66 deletions(-) diff --git a/config/crds/v1/all-crds.yaml b/config/crds/v1/all-crds.yaml index 41e77a1e88..ea0c433e19 100644 --- a/config/crds/v1/all-crds.yaml +++ b/config/crds/v1/all-crds.yaml @@ -226,7 +226,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -250,6 +251,7 @@ spec: description: |- FleetServerRef is a reference to Fleet Server that this Agent should connect to to obtain it's configuration. Don't set unless `mode` is set to `fleet`. + References to Fleet servers running outside the Kubernetes cluster via the `secretName` attribute are not supported. properties: name: description: Name of an existing Kubernetes object corresponding @@ -262,7 +264,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -713,7 +716,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -1206,7 +1210,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -1656,7 +1661,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -2577,7 +2583,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -2612,7 +2619,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -2657,7 +2665,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -2698,7 +2707,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -2917,7 +2927,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -4314,7 +4325,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -4355,7 +4367,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -6647,7 +6660,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -7225,7 +7239,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -7795,7 +7810,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -7826,7 +7842,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -8290,7 +8307,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -8331,7 +8349,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -9124,7 +9143,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -9176,7 +9196,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -9217,7 +9238,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource diff --git a/config/crds/v1/resources/agent.k8s.elastic.co_agents.yaml b/config/crds/v1/resources/agent.k8s.elastic.co_agents.yaml index 6aef4d0804..8b18bfb341 100644 --- a/config/crds/v1/resources/agent.k8s.elastic.co_agents.yaml +++ b/config/crds/v1/resources/agent.k8s.elastic.co_agents.yaml @@ -16477,7 +16477,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -16501,6 +16502,7 @@ spec: description: |- FleetServerRef is a reference to Fleet Server that this Agent should connect to to obtain it's configuration. Don't set unless `mode` is set to `fleet`. + References to Fleet servers running outside the Kubernetes cluster via the `secretName` attribute are not supported. properties: name: description: Name of an existing Kubernetes object corresponding @@ -16513,7 +16515,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -16964,7 +16967,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource diff --git a/config/crds/v1/resources/apm.k8s.elastic.co_apmservers.yaml b/config/crds/v1/resources/apm.k8s.elastic.co_apmservers.yaml index e29e6a1289..06529c2d20 100644 --- a/config/crds/v1/resources/apm.k8s.elastic.co_apmservers.yaml +++ b/config/crds/v1/resources/apm.k8s.elastic.co_apmservers.yaml @@ -81,7 +81,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -531,7 +532,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource diff --git a/config/crds/v1/resources/beat.k8s.elastic.co_beats.yaml b/config/crds/v1/resources/beat.k8s.elastic.co_beats.yaml index cebc5c508a..d4f5713a67 100644 --- a/config/crds/v1/resources/beat.k8s.elastic.co_beats.yaml +++ b/config/crds/v1/resources/beat.k8s.elastic.co_beats.yaml @@ -16477,7 +16477,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -16512,7 +16513,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -16557,7 +16559,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -16598,7 +16601,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource diff --git a/config/crds/v1/resources/elasticsearch.k8s.elastic.co_elasticsearches.yaml b/config/crds/v1/resources/elasticsearch.k8s.elastic.co_elasticsearches.yaml index cf14063fd0..d22f87b3f4 100644 --- a/config/crds/v1/resources/elasticsearch.k8s.elastic.co_elasticsearches.yaml +++ b/config/crds/v1/resources/elasticsearch.k8s.elastic.co_elasticsearches.yaml @@ -543,7 +543,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -584,7 +585,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource diff --git a/config/crds/v1/resources/enterprisesearch.k8s.elastic.co_enterprisesearches.yaml b/config/crds/v1/resources/enterprisesearch.k8s.elastic.co_enterprisesearches.yaml index 760880304e..a8ae5aeafc 100644 --- a/config/crds/v1/resources/enterprisesearch.k8s.elastic.co_enterprisesearches.yaml +++ b/config/crds/v1/resources/enterprisesearch.k8s.elastic.co_enterprisesearches.yaml @@ -92,7 +92,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -8731,7 +8732,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource diff --git a/config/crds/v1/resources/kibana.k8s.elastic.co_kibanas.yaml b/config/crds/v1/resources/kibana.k8s.elastic.co_kibanas.yaml index 7ff7d5ef45..3e6d2d3c15 100644 --- a/config/crds/v1/resources/kibana.k8s.elastic.co_kibanas.yaml +++ b/config/crds/v1/resources/kibana.k8s.elastic.co_kibanas.yaml @@ -81,7 +81,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -112,7 +113,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -576,7 +578,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -617,7 +620,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource diff --git a/config/crds/v1/resources/logstash.k8s.elastic.co_logstashes.yaml b/config/crds/v1/resources/logstash.k8s.elastic.co_logstashes.yaml index 3b183f24a3..8bd35fc203 100644 --- a/config/crds/v1/resources/logstash.k8s.elastic.co_logstashes.yaml +++ b/config/crds/v1/resources/logstash.k8s.elastic.co_logstashes.yaml @@ -105,7 +105,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -157,7 +158,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -198,7 +200,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource diff --git a/config/crds/v1/resources/maps.k8s.elastic.co_elasticmapsservers.yaml b/config/crds/v1/resources/maps.k8s.elastic.co_elasticmapsservers.yaml index af4f4d52ca..507aad169b 100644 --- a/config/crds/v1/resources/maps.k8s.elastic.co_elasticmapsservers.yaml +++ b/config/crds/v1/resources/maps.k8s.elastic.co_elasticmapsservers.yaml @@ -93,7 +93,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource 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 4373e77706..2c76f0bd2f 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 @@ -233,7 +233,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -257,6 +258,7 @@ spec: description: |- FleetServerRef is a reference to Fleet Server that this Agent should connect to to obtain it's configuration. Don't set unless `mode` is set to `fleet`. + References to Fleet servers running outside the Kubernetes cluster via the `secretName` attribute are not supported. properties: name: description: Name of an existing Kubernetes object corresponding @@ -269,7 +271,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -720,7 +723,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -1220,7 +1224,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -1670,7 +1675,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -2598,7 +2604,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -2633,7 +2640,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -2678,7 +2686,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -2719,7 +2728,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -2945,7 +2955,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -4356,7 +4367,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -4397,7 +4409,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -6696,7 +6709,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -7274,7 +7288,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -7851,7 +7866,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -7882,7 +7898,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -8346,7 +8363,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -8387,7 +8405,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -9187,7 +9206,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -9239,7 +9259,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource @@ -9280,7 +9301,8 @@ spec: secretName: description: |- SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - Elastic resource not managed by the operator. The referenced secret must contain the following: + Elastic resource not managed by the operator. + The referenced secret must contain the following: - `url`: the URL to reach the Elastic resource - `username`: the username of the user to be authenticated to the Elastic resource - `password`: the password of the user to be authenticated to the Elastic resource diff --git a/docs/reference/api-reference/main.md b/docs/reference/api-reference/main.md index 67eb56539d..ca2e37a9cd 100644 --- a/docs/reference/api-reference/main.md +++ b/docs/reference/api-reference/main.md @@ -92,7 +92,7 @@ AgentSpec defines the desired state of the Agent | *`fleetServerEnabled`* __boolean__ | FleetServerEnabled determines whether this Agent will launch Fleet Server. Don't set unless `mode` is set to `fleet`. | | *`policyID`* __string__ | PolicyID determines into which Agent Policy this Agent will be enrolled.
This field will become mandatory in a future release, default policies are deprecated since 8.1.0. | | *`kibanaRef`* __[ObjectSelector](#objectselector)__ | KibanaRef is a reference to Kibana where Fleet should be set up and this Agent should be enrolled. Don't set
unless `mode` is set to `fleet`. | -| *`fleetServerRef`* __[ObjectSelector](#objectselector)__ | FleetServerRef is a reference to Fleet Server that this Agent should connect to to obtain it's configuration.
Don't set unless `mode` is set to `fleet`. | +| *`fleetServerRef`* __[ObjectSelector](#objectselector)__ | FleetServerRef is a reference to Fleet Server that this Agent should connect to to obtain it's configuration.
Don't set unless `mode` is set to `fleet`.
References to Fleet servers running outside the Kubernetes cluster via the `secretName` attribute are not supported. | ### DaemonSetSpec [#daemonsetspec] @@ -588,7 +588,7 @@ or a Secret describing an external Elastic resource not managed by the operator. | *`namespace`* __string__ | Namespace of the Kubernetes object. If empty, defaults to the current namespace. | | *`name`* __string__ | Name of an existing Kubernetes object corresponding to an Elastic resource managed by ECK. | | *`serviceName`* __string__ | ServiceName is the name of an existing Kubernetes service which is used to make requests to the referenced
object. It has to be in the same namespace as the referenced resource. If left empty, the default HTTP service of
the referenced resource is used. | -| *`secretName`* __string__ | SecretName is the name of an existing Kubernetes secret that contains connection information for associating an
Elastic resource not managed by the operator. The referenced secret must contain the following:
- `url`: the URL to reach the Elastic resource
- `username`: the username of the user to be authenticated to the Elastic resource
- `password`: the password of the user to be authenticated to the Elastic resource
- `ca.crt`: the CA certificate in PEM format (optional)
- `api-key`: the key to authenticate against the Elastic resource instead of a username and password (supported only for `elasticsearchRefs` in AgentSpec and in BeatSpec)
This field cannot be used in combination with the other fields name, namespace or serviceName. | +| *`secretName`* __string__ | SecretName is the name of an existing Kubernetes secret that contains connection information for associating an
Elastic resource not managed by the operator.
The referenced secret must contain the following:
- `url`: the URL to reach the Elastic resource
- `username`: the username of the user to be authenticated to the Elastic resource
- `password`: the password of the user to be authenticated to the Elastic resource
- `ca.crt`: the CA certificate in PEM format (optional)
- `api-key`: the key to authenticate against the Elastic resource instead of a username and password (supported only for `elasticsearchRefs` in AgentSpec and in BeatSpec)
This field cannot be used in combination with the other fields name, namespace or serviceName. | ### PodDisruptionBudgetTemplate [#poddisruptionbudgettemplate] diff --git a/pkg/apis/agent/v1alpha1/agent_types.go b/pkg/apis/agent/v1alpha1/agent_types.go index 3d20837f00..f6fb7d4ac0 100644 --- a/pkg/apis/agent/v1alpha1/agent_types.go +++ b/pkg/apis/agent/v1alpha1/agent_types.go @@ -106,6 +106,7 @@ type AgentSpec struct { // FleetServerRef is a reference to Fleet Server that this Agent should connect to to obtain it's configuration. // Don't set unless `mode` is set to `fleet`. + // References to Fleet servers running outside the Kubernetes cluster via the `secretName` attribute are not supported. // +kubebuilder:validation:Optional FleetServerRef commonv1.ObjectSelector `json:"fleetServerRef,omitempty"` } diff --git a/pkg/apis/common/v1/common.go b/pkg/apis/common/v1/common.go index e2fc6bc292..cecd1a6149 100644 --- a/pkg/apis/common/v1/common.go +++ b/pkg/apis/common/v1/common.go @@ -115,7 +115,8 @@ type ObjectSelector struct { ServiceName string `json:"serviceName,omitempty"` // SecretName is the name of an existing Kubernetes secret that contains connection information for associating an - // Elastic resource not managed by the operator. The referenced secret must contain the following: + // Elastic resource not managed by the operator. + // The referenced secret must contain the following: // - `url`: the URL to reach the Elastic resource // - `username`: the username of the user to be authenticated to the Elastic resource // - `password`: the password of the user to be authenticated to the Elastic resource From f18b19eb79dd5d94153b871b18fca49a03092b92 Mon Sep 17 00:00:00 2001 From: tehbooom Date: Mon, 18 Aug 2025 14:38:42 -0400 Subject: [PATCH 04/14] fix: Removed comment stating that 0 is highest priority since negative values are allowed --- pkg/apis/stackconfigpolicy/v1alpha1/stackconfigpolicy_types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/apis/stackconfigpolicy/v1alpha1/stackconfigpolicy_types.go b/pkg/apis/stackconfigpolicy/v1alpha1/stackconfigpolicy_types.go index e4efae1c30..e7d17e2007 100644 --- a/pkg/apis/stackconfigpolicy/v1alpha1/stackconfigpolicy_types.go +++ b/pkg/apis/stackconfigpolicy/v1alpha1/stackconfigpolicy_types.go @@ -56,7 +56,7 @@ type StackConfigPolicyList struct { type StackConfigPolicySpec struct { ResourceSelector metav1.LabelSelector `json:"resourceSelector,omitempty"` // Weight determines the priority of this policy when multiple policies target the same resource. - // Lower weight values take precedence (0 is highest priority). Defaults to 0. + // Lower weight values take precedence. Defaults to 0. // +kubebuilder:default=0 Weight int32 `json:"weight,omitempty"` // Deprecated: SecureSettings only applies to Elasticsearch and is deprecated. It must be set per application instead. From e35848f0e6c4b936912510dfdc2bd4c4dc3ccdf1 Mon Sep 17 00:00:00 2001 From: tehbooom Date: Mon, 18 Aug 2025 14:44:00 -0400 Subject: [PATCH 05/14] fix: Update comments --- pkg/controller/elasticsearch/filesettings/file_settings.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pkg/controller/elasticsearch/filesettings/file_settings.go b/pkg/controller/elasticsearch/filesettings/file_settings.go index 74b03ee18f..74b7e67085 100644 --- a/pkg/controller/elasticsearch/filesettings/file_settings.go +++ b/pkg/controller/elasticsearch/filesettings/file_settings.go @@ -90,7 +90,7 @@ func (s *Settings) updateStateFromPolicies(es types.NamespacedName, policies []p sortedPolicies := make([]policyv1alpha1.StackConfigPolicy, len(policies)) copy(sortedPolicies, policies) - // Simple bubble sort by weight (descending order) + // Bubble sort by weight (descending order) for i := 0; i < len(sortedPolicies)-1; i++ { for j := 0; j < len(sortedPolicies)-i-1; j++ { if sortedPolicies[j].Spec.Weight < sortedPolicies[j+1].Spec.Weight { @@ -99,7 +99,6 @@ func (s *Settings) updateStateFromPolicies(es types.NamespacedName, policies []p } } - // Merge settings from all policies in order of decreasing weight (lower weight = higher priority) for _, policy := range sortedPolicies { if err := s.updateState(es, policy); err != nil { return err @@ -169,11 +168,9 @@ func (s *Settings) mergeConfig(target, source *commonv1.Config) { } for key, value := range source.Data { - // Check if both target and source values are maps (like snapshot repositories) if targetValue, exists := target.Data[key]; exists { if targetMap, targetIsMap := targetValue.(map[string]interface{}); targetIsMap { if sourceMap, sourceIsMap := value.(map[string]interface{}); sourceIsMap { - // Deep merge maps - source entries take precedence for subKey, subValue := range sourceMap { targetMap[subKey] = subValue } From 35d4caf570f1218422cf2bb0cb8b3eea3e8b38e7 Mon Sep 17 00:00:00 2001 From: tehbooom Date: Mon, 18 Aug 2025 15:17:15 -0400 Subject: [PATCH 06/14] docs: Updated API comments --- config/crds/v1/all-crds.yaml | 2 +- .../stackconfigpolicy.k8s.elastic.co_stackconfigpolicies.yaml | 2 +- .../charts/eck-operator-crds/templates/all-crds.yaml | 2 +- docs/reference/api-reference/main.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/crds/v1/all-crds.yaml b/config/crds/v1/all-crds.yaml index ea0c433e19..68c0d90d8d 100644 --- a/config/crds/v1/all-crds.yaml +++ b/config/crds/v1/all-crds.yaml @@ -10568,7 +10568,7 @@ spec: default: 0 description: |- Weight determines the priority of this policy when multiple policies target the same resource. - Lower weight values take precedence (0 is highest priority). Defaults to 0. + Lower weight values take precedence. Defaults to 0. format: int32 type: integer type: object diff --git a/config/crds/v1/resources/stackconfigpolicy.k8s.elastic.co_stackconfigpolicies.yaml b/config/crds/v1/resources/stackconfigpolicy.k8s.elastic.co_stackconfigpolicies.yaml index 26ba3cd041..826ca44b2d 100644 --- a/config/crds/v1/resources/stackconfigpolicy.k8s.elastic.co_stackconfigpolicies.yaml +++ b/config/crds/v1/resources/stackconfigpolicy.k8s.elastic.co_stackconfigpolicies.yaml @@ -292,7 +292,7 @@ spec: default: 0 description: |- Weight determines the priority of this policy when multiple policies target the same resource. - Lower weight values take precedence (0 is highest priority). Defaults to 0. + Lower weight values take precedence. Defaults to 0. format: int32 type: integer type: object 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 2c76f0bd2f..51898dcbb3 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 @@ -10638,7 +10638,7 @@ spec: default: 0 description: |- Weight determines the priority of this policy when multiple policies target the same resource. - Lower weight values take precedence (0 is highest priority). Defaults to 0. + Lower weight values take precedence. Defaults to 0. format: int32 type: integer type: object diff --git a/docs/reference/api-reference/main.md b/docs/reference/api-reference/main.md index ca2e37a9cd..ea44c8a217 100644 --- a/docs/reference/api-reference/main.md +++ b/docs/reference/api-reference/main.md @@ -2066,7 +2066,7 @@ StackConfigPolicy represents a StackConfigPolicy resource in a Kubernetes cluste | Field | Description | | --- | --- | | *`resourceSelector`* __[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#labelselector-v1-meta)__ | | -| *`weight`* __integer__ | Weight determines the priority of this policy when multiple policies target the same resource.
Lower weight values take precedence (0 is highest priority). Defaults to 0. | +| *`weight`* __integer__ | Weight determines the priority of this policy when multiple policies target the same resource.
Lower weight values take precedence. Defaults to 0. | | *`secureSettings`* __[SecretSource](#secretsource) array__ | Deprecated: SecureSettings only applies to Elasticsearch and is deprecated. It must be set per application instead. | | *`elasticsearch`* __[ElasticsearchConfigPolicySpec](#elasticsearchconfigpolicyspec)__ | | | *`kibana`* __[KibanaConfigPolicySpec](#kibanaconfigpolicyspec)__ | | From cc21222c73a241de2d8e090cecbe440697ed4ada Mon Sep 17 00:00:00 2001 From: tehbooom Date: Mon, 18 Aug 2025 15:46:09 -0400 Subject: [PATCH 07/14] fix: linting --- .../elasticsearch/filesettings/secret.go | 28 +++---- .../stackconfigpolicy/controller.go | 68 ++++++++-------- .../elasticsearch_config_settings.go | 77 ++----------------- 3 files changed, 57 insertions(+), 116 deletions(-) diff --git a/pkg/controller/elasticsearch/filesettings/secret.go b/pkg/controller/elasticsearch/filesettings/secret.go index e94a01d3dd..9f512d7321 100644 --- a/pkg/controller/elasticsearch/filesettings/secret.go +++ b/pkg/controller/elasticsearch/filesettings/secret.go @@ -92,7 +92,7 @@ func newSettingsSecretFromPolicies(version int64, es types.NamespacedName, curre } // Store all policy references in the secret - var policyRefs []PolicyRef + policyRefs := make([]PolicyRef, 0, len(policies)) for _, policy := range policies { policyRefs = append(policyRefs, PolicyRef{ Name: policy.Name, @@ -242,7 +242,7 @@ func setSecureSettings(settingsSecret *corev1.Secret, policy policyv1alpha1.Stac // setSecureSettingsFromPolicies sets secure settings from multiple policies into the settings secret func setSecureSettingsFromPolicies(settingsSecret *corev1.Secret, policies []policyv1alpha1.StackConfigPolicy) error { var allSecretSources []commonv1.NamespacedSecretSource //nolint:prealloc - + for _, policy := range policies { // Common secureSettings field, this is mainly there to maintain backwards compatibility //nolint:staticcheck @@ -255,7 +255,7 @@ func setSecureSettingsFromPolicies(settingsSecret *corev1.Secret, policies []pol allSecretSources = append(allSecretSources, commonv1.NamespacedSecretSource{Namespace: policy.GetNamespace(), SecretName: src.SecretName, Entries: src.Entries}) } } - + if len(allSecretSources) == 0 { return nil } @@ -293,17 +293,17 @@ func GetPolicyRefs(secret corev1.Secret) ([]PolicyRef, error) { if secret.Annotations == nil { return nil, nil } - + policiesData, ok := secret.Annotations["stackconfigpolicy.k8s.elastic.co/policies"] if !ok { return nil, nil } - + var policies []PolicyRef if err := json.Unmarshal([]byte(policiesData), &policies); err != nil { return nil, err } - + return policies, nil } @@ -312,12 +312,12 @@ func SetPolicyRefs(secret *corev1.Secret, policies []PolicyRef) error { if secret.Annotations == nil { secret.Annotations = make(map[string]string) } - + data, err := json.Marshal(policies) if err != nil { return err } - + secret.Annotations["stackconfigpolicy.k8s.elastic.co/policies"] = string(data) return nil } @@ -328,13 +328,13 @@ func AddOrUpdatePolicyRef(secret *corev1.Secret, policy policyv1alpha1.StackConf if err != nil { return err } - + policyRef := PolicyRef{ Name: policy.Name, Namespace: policy.Namespace, Weight: policy.Spec.Weight, } - + // Update existing policy or add new one found := false for i, p := range policies { @@ -344,11 +344,11 @@ func AddOrUpdatePolicyRef(secret *corev1.Secret, policy policyv1alpha1.StackConf break } } - + if !found { policies = append(policies, policyRef) } - + return SetPolicyRefs(secret, policies) } @@ -358,14 +358,14 @@ func RemovePolicyRef(secret *corev1.Secret, policyName, policyNamespace string) if err != nil { return err } - + var filtered []PolicyRef for _, p := range policies { if !(p.Name == policyName && p.Namespace == policyNamespace) { filtered = append(filtered, p) } } - + return SetPolicyRefs(secret, filtered) } diff --git a/pkg/controller/stackconfigpolicy/controller.go b/pkg/controller/stackconfigpolicy/controller.go index 2609b0bc51..34e4928dd0 100644 --- a/pkg/controller/stackconfigpolicy/controller.go +++ b/pkg/controller/stackconfigpolicy/controller.go @@ -389,19 +389,20 @@ func (r *ReconcileStackConfigPolicy) reconcileElasticsearchResources(ctx context // extract the metadata that should be propagated to children meta := metadata.Propagate(&es, metadata.Metadata{Labels: eslabel.NewLabels(k8s.ExtractNamespacedName(&es))}) - + // create the expected Settings Secret from all applicable policies var expectedSecret corev1.Secret var expectedVersion int64 - if len(allPolicies) > 1 { - // Multiple policies - use the multi-policy approach - expectedSecret, expectedVersion, err = filesettings.NewSettingsSecretWithVersionFromPolicies(esNsn, &actualSettingsSecret, allPolicies, meta) - } else if len(allPolicies) == 1 { - // Single policy - use the original approach for backward compatibility - expectedSecret, expectedVersion, err = filesettings.NewSettingsSecretWithVersion(esNsn, &actualSettingsSecret, &allPolicies[0], meta) - } else { + switch len(allPolicies) { + case 0: // No policies target this resource - skip (shouldn't happen in practice) continue + case 1: + // Single policy - use the original approach for backward compatibility + expectedSecret, expectedVersion, err = filesettings.NewSettingsSecretWithVersion(esNsn, &actualSettingsSecret, &allPolicies[0], meta) + default: + // Multiple policies - use the multi-policy approach + expectedSecret, expectedVersion, err = filesettings.NewSettingsSecretWithVersionFromPolicies(esNsn, &actualSettingsSecret, allPolicies, meta) } if err != nil { return results.WithError(err), status @@ -524,18 +525,21 @@ func (r *ReconcileStackConfigPolicy) reconcileKibanaResources(ctx context.Contex if hasKibanaConfig { // Only add to configured resources if at least one policy has Kibana config set. configuredResources[kibanaNsn] = kibana - + var expectedConfigSecret corev1.Secret - if len(allPolicies) > 1 { - // Multiple policies - use the multi-policy approach - expectedConfigSecret, err = r.newKibanaConfigSecretFromPolicies(allPolicies, kibana) - } else if len(allPolicies) == 1 { - // Single policy - use the original approach for backward compatibility - expectedConfigSecret, err = newKibanaConfigSecret(allPolicies[0], kibana) - } else { + + switch len(allPolicies) { + case 0: // No policies target this resource - skip (shouldn't happen in practice) continue + case 1: + // Single policy - use the original approach for backward compatibility + expectedConfigSecret, err = newKibanaConfigSecret(allPolicies[0], kibana) + default: + // Multiple policies - use the multi-policy approach + expectedConfigSecret, err = r.newKibanaConfigSecretFromPolicies(allPolicies, kibana) } + if err != nil { return results.WithError(err), status } @@ -875,7 +879,7 @@ func (r *ReconcileStackConfigPolicy) checkWeightConflicts(ctx context.Context, p } for _, otherPolicy := range conflictingPolicies { - if r.policiesCouldOverlap(ctx, policy, &otherPolicy, policySelector) { + if r.policiesCouldOverlap(policy, &otherPolicy, policySelector) { // Check if the policies have conflicting settings if r.policiesHaveConflictingSettings(policy, &otherPolicy) { return fmt.Errorf("weight conflict detected: StackConfigPolicy %s/%s has the same weight (%d) and would overwrite conflicting settings. Policies with the same weight that target overlapping resources must configure different, non-conflicting settings", @@ -895,17 +899,17 @@ func (r *ReconcileStackConfigPolicy) policiesHaveConflictingSettings(policy1, po if r.policyIsEmpty(policy1) && r.policyIsEmpty(policy2) { return true // Both empty policies would conflict in the same namespace/selectors } - + // Check Elasticsearch settings for conflicts if r.elasticsearchSettingsConflict(policy1, policy2) { return true } - + // Check Kibana settings for conflicts if r.kibanaSettingsConflict(policy1, policy2) { return true } - + return false } @@ -913,7 +917,7 @@ func (r *ReconcileStackConfigPolicy) policiesHaveConflictingSettings(policy1, po func (r *ReconcileStackConfigPolicy) policyIsEmpty(policy *policyv1alpha1.StackConfigPolicy) bool { es := &policy.Spec.Elasticsearch kb := &policy.Spec.Kibana - + // Check if Elasticsearch settings are empty esEmpty := (es.ClusterSettings == nil || len(es.ClusterSettings.Data) == 0) && (es.SnapshotRepositories == nil || len(es.SnapshotRepositories.Data) == 0) && @@ -925,10 +929,10 @@ func (r *ReconcileStackConfigPolicy) policyIsEmpty(policy *policyv1alpha1.StackC (es.IndexTemplates.ComposableIndexTemplates == nil || len(es.IndexTemplates.ComposableIndexTemplates.Data) == 0) && (es.Config == nil || len(es.Config.Data) == 0) && len(es.SecretMounts) == 0 - + // Check if Kibana settings are empty kbEmpty := (kb.Config == nil || len(kb.Config.Data) == 0) - + return esEmpty && kbEmpty } @@ -936,7 +940,7 @@ func (r *ReconcileStackConfigPolicy) policyIsEmpty(policy *policyv1alpha1.StackC func (r *ReconcileStackConfigPolicy) elasticsearchSettingsConflict(policy1, policy2 *policyv1alpha1.StackConfigPolicy) bool { es1 := &policy1.Spec.Elasticsearch es2 := &policy2.Spec.Elasticsearch - + // Check each type of setting for key conflicts if r.configsConflict(es1.ClusterSettings, es2.ClusterSettings) { return true @@ -965,7 +969,7 @@ func (r *ReconcileStackConfigPolicy) elasticsearchSettingsConflict(policy1, poli if r.configsConflict(es1.Config, es2.Config) { return true } - + // Check secret mounts for path conflicts return r.secretMountsConflict(es1.SecretMounts, es2.SecretMounts) } @@ -974,7 +978,7 @@ func (r *ReconcileStackConfigPolicy) elasticsearchSettingsConflict(policy1, poli func (r *ReconcileStackConfigPolicy) kibanaSettingsConflict(policy1, policy2 *policyv1alpha1.StackConfigPolicy) bool { kb1 := &policy1.Spec.Kibana kb2 := &policy2.Spec.Kibana - + return r.configsConflict(kb1.Config, kb2.Config) } @@ -983,14 +987,14 @@ func (r *ReconcileStackConfigPolicy) configsConflict(config1, config2 *commonv1. if config1 == nil || config2 == nil || config1.Data == nil || config2.Data == nil { return false } - + // Check if there are any common keys for key := range config1.Data { if _, exists := config2.Data[key]; exists { return true } } - + return false } @@ -999,23 +1003,23 @@ func (r *ReconcileStackConfigPolicy) secretMountsConflict(mounts1, mounts2 []pol if len(mounts1) == 0 || len(mounts2) == 0 { return false } - + paths1 := make(map[string]bool) for _, mount := range mounts1 { paths1[mount.MountPath] = true } - + for _, mount := range mounts2 { if paths1[mount.MountPath] { return true } } - + return false } // policiesCouldOverlap checks if two policies could potentially target the same resources -func (r *ReconcileStackConfigPolicy) policiesCouldOverlap(ctx context.Context, policy1, policy2 *policyv1alpha1.StackConfigPolicy, policy1Selector labels.Selector) bool { +func (r *ReconcileStackConfigPolicy) policiesCouldOverlap(policy1, policy2 *policyv1alpha1.StackConfigPolicy, policy1Selector labels.Selector) bool { // Check namespace-based restrictions first if !r.namespacesCouldOverlap(policy1.Namespace, policy2.Namespace) { return false diff --git a/pkg/controller/stackconfigpolicy/elasticsearch_config_settings.go b/pkg/controller/stackconfigpolicy/elasticsearch_config_settings.go index 120d936871..836078057e 100644 --- a/pkg/controller/stackconfigpolicy/elasticsearch_config_settings.go +++ b/pkg/controller/stackconfigpolicy/elasticsearch_config_settings.go @@ -33,49 +33,6 @@ const ( SecretsMountKey = "secretMounts.json" ) -func newElasticsearchConfigSecret(policy policyv1alpha1.StackConfigPolicy, es esv1.Elasticsearch) (corev1.Secret, error) { - data := make(map[string][]byte) - if len(policy.Spec.Elasticsearch.SecretMounts) > 0 { - secretMountBytes, err := json.Marshal(policy.Spec.Elasticsearch.SecretMounts) - if err != nil { - return corev1.Secret{}, err - } - data[SecretsMountKey] = secretMountBytes - } - - elasticsearchAndMountsConfigHash := getElasticsearchConfigAndMountsHash(policy.Spec.Elasticsearch.Config, policy.Spec.Elasticsearch.SecretMounts) - if policy.Spec.Elasticsearch.Config != nil { - configDataJSONBytes, err := policy.Spec.Elasticsearch.Config.MarshalJSON() - if err != nil { - return corev1.Secret{}, err - } - data[ElasticSearchConfigKey] = configDataJSONBytes - } - meta := metadata.Propagate(&es, metadata.Metadata{ - Labels: eslabel.NewLabels(k8s.ExtractNamespacedName(&es)), - Annotations: map[string]string{ - commonannotation.ElasticsearchConfigAndSecretMountsHashAnnotation: elasticsearchAndMountsConfigHash, - }, - }) - elasticsearchConfigSecret := corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: es.Namespace, - Name: esv1.StackConfigElasticsearchConfigSecretName(es.Name), - Labels: meta.Labels, - Annotations: meta.Annotations, - }, - Data: data, - } - - // Set StackConfigPolicy 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.OrphanSecretDeleteOnPolicyDelete - - 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, meta metadata.Metadata) error { for _, secretMount := range policy.Spec.Elasticsearch.SecretMounts { @@ -125,26 +82,6 @@ func getElasticsearchConfigAndMountsHash(elasticsearchConfig *commonv1.Config, s return hash.HashObject(secretMounts) } -// 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.InNamespace(es.Namespace), 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[commonannotation.ElasticsearchConfigAndSecretMountsHashAnnotation] != elasticsearchAndMountsConfigHash { - return false, nil - } - } - - return true, nil -} - // Multi-policy versions of the above functions // newElasticsearchConfigSecretFromPolicies creates an Elasticsearch config secret from multiple policies @@ -152,7 +89,7 @@ func (r *ReconcileStackConfigPolicy) newElasticsearchConfigSecretFromPolicies(po data := make(map[string][]byte) var allSecretMounts []policyv1alpha1.SecretMount var mergedConfig *commonv1.Config - + // Sort policies by weight (descending) so lower weights override higher ones sortedPolicies := make([]policyv1alpha1.StackConfigPolicy, len(policies)) copy(sortedPolicies, policies) @@ -163,11 +100,11 @@ func (r *ReconcileStackConfigPolicy) newElasticsearchConfigSecretFromPolicies(po } } } - + // Merge secret mounts from all policies for _, policy := range sortedPolicies { allSecretMounts = append(allSecretMounts, policy.Spec.Elasticsearch.SecretMounts...) - + // Merge Elasticsearch configs (lower weight policies override higher ones) if policy.Spec.Elasticsearch.Config != nil { if mergedConfig == nil { @@ -180,7 +117,7 @@ func (r *ReconcileStackConfigPolicy) newElasticsearchConfigSecretFromPolicies(po } } } - + // Add secret mounts to data if any exist if len(allSecretMounts) > 0 { secretMountBytes, err := json.Marshal(allSecretMounts) @@ -217,7 +154,7 @@ func (r *ReconcileStackConfigPolicy) newElasticsearchConfigSecretFromPolicies(po } // Store all policy references in the secret - var policyRefs []filesettings.PolicyRef + policyRefs := make([]filesettings.PolicyRef, 0, len(policies)) for _, policy := range policies { policyRefs = append(policyRefs, filesettings.PolicyRef{ Name: policy.Name, @@ -278,7 +215,7 @@ func (r *ReconcileStackConfigPolicy) reconcileSecretMountsFromPolicies(ctx conte commonannotation.SourceSecretAnnotationName: secretMount.SecretName, }, }) - + // Recreate it in the Elasticsearch namespace, prefix with es name. secretName := esv1.StackConfigAdditionalSecretName(es.Name, secretMount.SecretName) expected := corev1.Secret{ @@ -292,7 +229,7 @@ func (r *ReconcileStackConfigPolicy) reconcileSecretMountsFromPolicies(ctx conte } // Store policy references that use this secret mount - var policyRefs []filesettings.PolicyRef + policyRefs := make([]filesettings.PolicyRef, 0, len(policies)) for _, policy := range policies { for _, mount := range policy.Spec.Elasticsearch.SecretMounts { if mount.SecretName == secretMount.SecretName { From a1b9c6c0ba3f350a2ebbdce6d47334e0b4fc974a Mon Sep 17 00:00:00 2001 From: tehbooom Date: Mon, 18 Aug 2025 16:01:17 -0400 Subject: [PATCH 08/14] fix: linting --- pkg/controller/stackconfigpolicy/controller.go | 16 ++++++++-------- .../stackconfigpolicy/kibana_config_settings.go | 14 +++++++------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pkg/controller/stackconfigpolicy/controller.go b/pkg/controller/stackconfigpolicy/controller.go index 34e4928dd0..6540c8197f 100644 --- a/pkg/controller/stackconfigpolicy/controller.go +++ b/pkg/controller/stackconfigpolicy/controller.go @@ -551,15 +551,15 @@ func (r *ReconcileStackConfigPolicy) reconcileKibanaResources(ctx context.Contex // Check if required Kibana configs from all policies are applied. var configApplied bool - if len(allPolicies) > 1 { - configApplied, err = r.kibanaConfigAppliedFromPolicies(allPolicies, kibana) - } else if len(allPolicies) == 1 { + + switch len(allPolicies) { + case 0: + // No policies, so nothing to apply + configApplied = true + case 1: configApplied, err = kibanaConfigApplied(r.Client, allPolicies[0], kibana) - } else { - configApplied = true // No policies, so nothing to apply - } - if err != nil { - return results.WithError(err), status + default: + configApplied, err = r.kibanaConfigAppliedFromPolicies(allPolicies, kibana) } // update the Kibana resource status for this Kibana diff --git a/pkg/controller/stackconfigpolicy/kibana_config_settings.go b/pkg/controller/stackconfigpolicy/kibana_config_settings.go index 24d079058f..8ad324c671 100644 --- a/pkg/controller/stackconfigpolicy/kibana_config_settings.go +++ b/pkg/controller/stackconfigpolicy/kibana_config_settings.go @@ -150,7 +150,7 @@ func setKibanaSecureSettings(settingsSecret *corev1.Secret, policy policyv1alpha // newKibanaConfigSecretFromPolicies creates a Kibana config secret from multiple policies func (r *ReconcileStackConfigPolicy) newKibanaConfigSecretFromPolicies(policies []policyv1alpha1.StackConfigPolicy, kibana kibanav1.Kibana) (corev1.Secret, error) { var mergedConfig *commonv1.Config - + // Sort policies by weight (descending) so lower weights override higher ones sortedPolicies := make([]policyv1alpha1.StackConfigPolicy, len(policies)) copy(sortedPolicies, policies) @@ -161,7 +161,7 @@ func (r *ReconcileStackConfigPolicy) newKibanaConfigSecretFromPolicies(policies } } } - + // Merge Kibana configs (lower weight policies override higher ones) for _, policy := range sortedPolicies { if policy.Spec.Kibana.Config != nil { @@ -175,7 +175,7 @@ func (r *ReconcileStackConfigPolicy) newKibanaConfigSecretFromPolicies(policies } } } - + kibanaConfigHash := getKibanaConfigHash(mergedConfig) configDataJSONBytes := []byte("") var err error @@ -184,7 +184,7 @@ func (r *ReconcileStackConfigPolicy) newKibanaConfigSecretFromPolicies(policies return corev1.Secret{}, err } } - + meta := metadata.Propagate(&kibana, metadata.Metadata{ Labels: kblabel.NewLabels(k8s.ExtractNamespacedName(&kibana)), Annotations: map[string]string{ @@ -204,7 +204,7 @@ func (r *ReconcileStackConfigPolicy) newKibanaConfigSecretFromPolicies(policies } // Store all policy references in the secret - var policyRefs []filesettings.PolicyRef + policyRefs := make([]filesettings.PolicyRef, 0, len(policies)) for _, policy := range policies { policyRefs = append(policyRefs, filesettings.PolicyRef{ Name: policy.Name, @@ -273,14 +273,14 @@ func (r *ReconcileStackConfigPolicy) kibanaConfigAppliedFromPolicies(policies [] // setKibanaSecureSettingsFromPolicies stores secure settings from multiple policies func (r *ReconcileStackConfigPolicy) setKibanaSecureSettingsFromPolicies(settingsSecret *corev1.Secret, policies []policyv1alpha1.StackConfigPolicy) error { var allSecretSources []commonv1.NamespacedSecretSource //nolint:prealloc - + for _, policy := range policies { // SecureSettings field under Kibana in the StackConfigPolicy for _, src := range policy.Spec.Kibana.SecureSettings { allSecretSources = append(allSecretSources, commonv1.NamespacedSecretSource{Namespace: policy.GetNamespace(), SecretName: src.SecretName, Entries: src.Entries}) } } - + if len(allSecretSources) == 0 { return nil } From a2563fdaf6b9b64d4f9e8ab238d43cb7d79eaba5 Mon Sep 17 00:00:00 2001 From: tehbooom Date: Tue, 19 Aug 2025 08:38:53 -0400 Subject: [PATCH 09/14] fix: lint with golangci-lint v1.64.5 --- pkg/controller/elasticsearch/filesettings/secret.go | 2 +- pkg/controller/stackconfigpolicy/controller.go | 3 +++ pkg/controller/stackconfigpolicy/controller_test.go | 2 +- pkg/controller/stackconfigpolicy/kibana_config_settings.go | 2 +- pkg/controller/stackconfigpolicy/weight_conflict_test.go | 1 - 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pkg/controller/elasticsearch/filesettings/secret.go b/pkg/controller/elasticsearch/filesettings/secret.go index 9f512d7321..ae7024a64d 100644 --- a/pkg/controller/elasticsearch/filesettings/secret.go +++ b/pkg/controller/elasticsearch/filesettings/secret.go @@ -241,7 +241,7 @@ func setSecureSettings(settingsSecret *corev1.Secret, policy policyv1alpha1.Stac // setSecureSettingsFromPolicies sets secure settings from multiple policies into the settings secret func setSecureSettingsFromPolicies(settingsSecret *corev1.Secret, policies []policyv1alpha1.StackConfigPolicy) error { - var allSecretSources []commonv1.NamespacedSecretSource //nolint:prealloc + var allSecretSources []commonv1.NamespacedSecretSource for _, policy := range policies { // Common secureSettings field, this is mainly there to maintain backwards compatibility diff --git a/pkg/controller/stackconfigpolicy/controller.go b/pkg/controller/stackconfigpolicy/controller.go index 6540c8197f..87d3ef38de 100644 --- a/pkg/controller/stackconfigpolicy/controller.go +++ b/pkg/controller/stackconfigpolicy/controller.go @@ -561,6 +561,9 @@ func (r *ReconcileStackConfigPolicy) reconcileKibanaResources(ctx context.Contex default: configApplied, err = r.kibanaConfigAppliedFromPolicies(allPolicies, kibana) } + if err != nil { + return results.WithError(err), status + } // update the Kibana resource status for this Kibana err = status.UpdateResourceStatusPhase(kibanaNsn, policyv1alpha1.ResourcePolicyStatus{}, configApplied, policyv1alpha1.KibanaResourceType) diff --git a/pkg/controller/stackconfigpolicy/controller_test.go b/pkg/controller/stackconfigpolicy/controller_test.go index 39b512b5a6..60d37b2939 100644 --- a/pkg/controller/stackconfigpolicy/controller_test.go +++ b/pkg/controller/stackconfigpolicy/controller_test.go @@ -397,7 +397,7 @@ func TestReconcileStackConfigPolicy_Reconcile(t *testing.T) { policy := r.getPolicy(t, k8s.ExtractNamespacedName(&policyFixture)) assert.Equal(t, 1, policy.Status.Resources) // The policy should show an error since Elasticsearch client might not be available in test - assert.Equal(t, 0, policy.Status.Ready) // Error state + assert.Equal(t, 0, policy.Status.Ready) // Error state assert.Equal(t, policyv1alpha1.ErrorPhase, policy.Status.Phase) // Error due to test limitations }, wantErr: false, diff --git a/pkg/controller/stackconfigpolicy/kibana_config_settings.go b/pkg/controller/stackconfigpolicy/kibana_config_settings.go index 8ad324c671..3da40562b0 100644 --- a/pkg/controller/stackconfigpolicy/kibana_config_settings.go +++ b/pkg/controller/stackconfigpolicy/kibana_config_settings.go @@ -272,7 +272,7 @@ func (r *ReconcileStackConfigPolicy) kibanaConfigAppliedFromPolicies(policies [] // setKibanaSecureSettingsFromPolicies stores secure settings from multiple policies func (r *ReconcileStackConfigPolicy) setKibanaSecureSettingsFromPolicies(settingsSecret *corev1.Secret, policies []policyv1alpha1.StackConfigPolicy) error { - var allSecretSources []commonv1.NamespacedSecretSource //nolint:prealloc + var allSecretSources []commonv1.NamespacedSecretSource for _, policy := range policies { // SecureSettings field under Kibana in the StackConfigPolicy diff --git a/pkg/controller/stackconfigpolicy/weight_conflict_test.go b/pkg/controller/stackconfigpolicy/weight_conflict_test.go index 523ce59914..ac5776c1eb 100644 --- a/pkg/controller/stackconfigpolicy/weight_conflict_test.go +++ b/pkg/controller/stackconfigpolicy/weight_conflict_test.go @@ -440,4 +440,3 @@ func TestNamespacesCouldOverlap(t *testing.T) { }) } } - From 8445011db94778d34ea87f5d15d1444a904827d0 Mon Sep 17 00:00:00 2001 From: tehbooom Date: Thu, 28 Aug 2025 13:45:07 -0400 Subject: [PATCH 10/14] fix: use sort.SliceStable for policies --- .../filesettings/file_settings.go | 13 +++++------- .../elasticsearch_config_settings.go | 21 +++++++------------ .../kibana_config_settings.go | 21 +++++++------------ 3 files changed, 19 insertions(+), 36 deletions(-) diff --git a/pkg/controller/elasticsearch/filesettings/file_settings.go b/pkg/controller/elasticsearch/filesettings/file_settings.go index 74b7e67085..0b43a49d2f 100644 --- a/pkg/controller/elasticsearch/filesettings/file_settings.go +++ b/pkg/controller/elasticsearch/filesettings/file_settings.go @@ -7,6 +7,7 @@ package filesettings import ( "fmt" "path/filepath" + "sort" "k8s.io/apimachinery/pkg/types" @@ -90,14 +91,10 @@ func (s *Settings) updateStateFromPolicies(es types.NamespacedName, policies []p sortedPolicies := make([]policyv1alpha1.StackConfigPolicy, len(policies)) copy(sortedPolicies, policies) - // Bubble sort by weight (descending order) - for i := 0; i < len(sortedPolicies)-1; i++ { - for j := 0; j < len(sortedPolicies)-i-1; j++ { - if sortedPolicies[j].Spec.Weight < sortedPolicies[j+1].Spec.Weight { - sortedPolicies[j], sortedPolicies[j+1] = sortedPolicies[j+1], sortedPolicies[j] - } - } - } + // sort by weight (descending order) + sort.SliceStable(sortedPolicies, func(i, j int) bool { + return sortedPolicies[i].Spec.Weight > sortedPolicies[j].Spec.Weight + }) for _, policy := range sortedPolicies { if err := s.updateState(es, policy); err != nil { diff --git a/pkg/controller/stackconfigpolicy/elasticsearch_config_settings.go b/pkg/controller/stackconfigpolicy/elasticsearch_config_settings.go index 836078057e..4d05b6f268 100644 --- a/pkg/controller/stackconfigpolicy/elasticsearch_config_settings.go +++ b/pkg/controller/stackconfigpolicy/elasticsearch_config_settings.go @@ -7,6 +7,7 @@ package stackconfigpolicy import ( "context" "encoding/json" + "sort" "sigs.k8s.io/controller-runtime/pkg/client" @@ -93,13 +94,9 @@ func (r *ReconcileStackConfigPolicy) newElasticsearchConfigSecretFromPolicies(po // Sort policies by weight (descending) so lower weights override higher ones sortedPolicies := make([]policyv1alpha1.StackConfigPolicy, len(policies)) copy(sortedPolicies, policies) - for i := 0; i < len(sortedPolicies)-1; i++ { - for j := 0; j < len(sortedPolicies)-i-1; j++ { - if sortedPolicies[j].Spec.Weight < sortedPolicies[j+1].Spec.Weight { - sortedPolicies[j], sortedPolicies[j+1] = sortedPolicies[j+1], sortedPolicies[j] - } - } - } + sort.SliceStable(sortedPolicies, func(i, j int) bool { + return sortedPolicies[i].Spec.Weight > sortedPolicies[j].Spec.Weight + }) // Merge secret mounts from all policies for _, policy := range sortedPolicies { @@ -273,13 +270,9 @@ func (r *ReconcileStackConfigPolicy) elasticsearchConfigAndSecretMountsAppliedFr // Sort policies by weight and merge (descending order) sortedPolicies := make([]policyv1alpha1.StackConfigPolicy, len(policies)) copy(sortedPolicies, policies) - for i := 0; i < len(sortedPolicies)-1; i++ { - for j := 0; j < len(sortedPolicies)-i-1; j++ { - if sortedPolicies[j].Spec.Weight < sortedPolicies[j+1].Spec.Weight { - sortedPolicies[j], sortedPolicies[j+1] = sortedPolicies[j+1], sortedPolicies[j] - } - } - } + sort.SliceStable(sortedPolicies, func(i, j int) bool { + return sortedPolicies[i].Spec.Weight > sortedPolicies[j].Spec.Weight + }) for _, policy := range sortedPolicies { allSecretMounts = append(allSecretMounts, policy.Spec.Elasticsearch.SecretMounts...) diff --git a/pkg/controller/stackconfigpolicy/kibana_config_settings.go b/pkg/controller/stackconfigpolicy/kibana_config_settings.go index 3da40562b0..76df874f3d 100644 --- a/pkg/controller/stackconfigpolicy/kibana_config_settings.go +++ b/pkg/controller/stackconfigpolicy/kibana_config_settings.go @@ -7,6 +7,7 @@ package stackconfigpolicy import ( "context" "encoding/json" + "sort" "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/metadata" @@ -154,13 +155,9 @@ func (r *ReconcileStackConfigPolicy) newKibanaConfigSecretFromPolicies(policies // Sort policies by weight (descending) so lower weights override higher ones sortedPolicies := make([]policyv1alpha1.StackConfigPolicy, len(policies)) copy(sortedPolicies, policies) - for i := 0; i < len(sortedPolicies)-1; i++ { - for j := 0; j < len(sortedPolicies)-i-1; j++ { - if sortedPolicies[j].Spec.Weight < sortedPolicies[j+1].Spec.Weight { - sortedPolicies[j], sortedPolicies[j+1] = sortedPolicies[j+1], sortedPolicies[j] - } - } - } + sort.SliceStable(sortedPolicies, func(i, j int) bool { + return sortedPolicies[i].Spec.Weight > sortedPolicies[j].Spec.Weight + }) // Merge Kibana configs (lower weight policies override higher ones) for _, policy := range sortedPolicies { @@ -240,13 +237,9 @@ func (r *ReconcileStackConfigPolicy) kibanaConfigAppliedFromPolicies(policies [] // Sort policies by weight and merge (descending order) sortedPolicies := make([]policyv1alpha1.StackConfigPolicy, len(policies)) copy(sortedPolicies, policies) - for i := 0; i < len(sortedPolicies)-1; i++ { - for j := 0; j < len(sortedPolicies)-i-1; j++ { - if sortedPolicies[j].Spec.Weight < sortedPolicies[j+1].Spec.Weight { - sortedPolicies[j], sortedPolicies[j+1] = sortedPolicies[j+1], sortedPolicies[j] - } - } - } + sort.SliceStable(sortedPolicies, func(i, j int) bool { + return sortedPolicies[i].Spec.Weight > sortedPolicies[j].Spec.Weight + }) for _, policy := range sortedPolicies { if policy.Spec.Kibana.Config != nil { From 9ea2fae5509223316302fcea2069dea53d7b4122 Mon Sep 17 00:00:00 2001 From: tehbooom Date: Thu, 28 Aug 2025 13:45:20 -0400 Subject: [PATCH 11/14] fix: function naming --- pkg/controller/elasticsearch/filesettings/secret.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/controller/elasticsearch/filesettings/secret.go b/pkg/controller/elasticsearch/filesettings/secret.go index ae7024a64d..b313852a25 100644 --- a/pkg/controller/elasticsearch/filesettings/secret.go +++ b/pkg/controller/elasticsearch/filesettings/secret.go @@ -43,11 +43,11 @@ func NewSettingsSecretWithVersion(es types.NamespacedName, currentSecret *corev1 // Policies are merged based on their weights, with higher weights taking precedence. func NewSettingsSecretWithVersionFromPolicies(es types.NamespacedName, currentSecret *corev1.Secret, policies []policyv1alpha1.StackConfigPolicy, meta metadata.Metadata) (corev1.Secret, int64, error) { newVersion := time.Now().UnixNano() - return newSettingsSecretFromPolicies(newVersion, es, currentSecret, policies, meta) + return NewSettingsSecretFromPolicies(newVersion, es, currentSecret, policies, meta) } // NewSettingsSecretFromPolicies returns a new SettingsSecret for a given Elasticsearch from multiple StackConfigPolicies. -func newSettingsSecretFromPolicies(version int64, es types.NamespacedName, currentSecret *corev1.Secret, policies []policyv1alpha1.StackConfigPolicy, meta metadata.Metadata) (corev1.Secret, int64, error) { +func NewSettingsSecretFromPolicies(version int64, es types.NamespacedName, currentSecret *corev1.Secret, policies []policyv1alpha1.StackConfigPolicy, meta metadata.Metadata) (corev1.Secret, int64, error) { settings := NewEmptySettings(version) // update the settings according to the config policies From f7613d2388aa43e6198aee32b0043aa9eac8e165 Mon Sep 17 00:00:00 2001 From: tehbooom Date: Thu, 28 Aug 2025 13:45:48 -0400 Subject: [PATCH 12/14] test: add e2e tests for multi-scp --- test/e2e/es/stackconfigpolicy_test.go | 217 ++++++++++++++++++++++++++ test/e2e/kb/stackconfigpolicy_test.go | 189 ++++++++++++++++++++++ 2 files changed, 406 insertions(+) diff --git a/test/e2e/es/stackconfigpolicy_test.go b/test/e2e/es/stackconfigpolicy_test.go index 01f48ef358..c878cdd3fd 100644 --- a/test/e2e/es/stackconfigpolicy_test.go +++ b/test/e2e/es/stackconfigpolicy_test.go @@ -336,6 +336,223 @@ func TestStackConfigPolicy(t *testing.T) { test.Sequence(nil, steps, esWithlicense).RunSequential(t) } +// TestStackConfigPolicyMultipleWeights tests multiple StackConfigPolicies with different weights. +func TestStackConfigPolicyMultipleWeights(t *testing.T) { + // only execute this test if we have a test license to work with + if test.Ctx().TestLicense == "" { + t.SkipNow() + } + + // StackConfigPolicy is supported for ES versions with file-based settings feature + stackVersion := version.MustParse(test.Ctx().ElasticStackVersion) + if !stackVersion.GTE(filesettings.FileBasedSettingsMinPreVersion) { + t.SkipNow() + } + + es := elasticsearch.NewBuilder("test-es-scp-multi"). + WithESMasterDataNodes(1, elasticsearch.DefaultResources). + WithLabel("app", "elasticsearch") + + namespace := test.Ctx().ManagedNamespace(0) + + // Policy with weight 10 (lower priority) - sets cluster.name + lowPriorityPolicy := policyv1alpha1.StackConfigPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: fmt.Sprintf("low-priority-scp-%s", rand.String(4)), + }, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 10, + ResourceSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "elasticsearch"}, + }, + Elasticsearch: policyv1alpha1.ElasticsearchConfigPolicySpec{ + Config: &commonv1.Config{ + Data: map[string]interface{}{ + "cluster.name": "low-priority-cluster", + }, + }, + ClusterSettings: &commonv1.Config{ + Data: map[string]interface{}{ + "persistent": map[string]interface{}{ + "indices.recovery.max_bytes_per_sec": "50mb", + }, + }, + }, + }, + }, + } + + // Policy with weight 20 (higher priority) - should override cluster.name and settings + highPriorityPolicy := policyv1alpha1.StackConfigPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: fmt.Sprintf("high-priority-scp-%s", rand.String(4)), + }, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 20, + ResourceSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "elasticsearch"}, + }, + Elasticsearch: policyv1alpha1.ElasticsearchConfigPolicySpec{ + Config: &commonv1.Config{ + Data: map[string]interface{}{ + "cluster.name": "high-priority-cluster", + }, + }, + ClusterSettings: &commonv1.Config{ + Data: map[string]interface{}{ + "persistent": map[string]interface{}{ + "indices.recovery.max_bytes_per_sec": "200mb", + }, + }, + }, + }, + }, + } + + // Policy with same weight 20 but different selector (should not conflict) + nonConflictingPolicy := policyv1alpha1.StackConfigPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: fmt.Sprintf("non-conflicting-scp-%s", rand.String(4)), + }, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 20, + ResourceSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "kibana"}, // Different selector + }, + Elasticsearch: policyv1alpha1.ElasticsearchConfigPolicySpec{ + Config: &commonv1.Config{ + Data: map[string]interface{}{ + "cluster.name": "should-not-apply", + }, + }, + }, + }, + } + + esWithlicense := test.LicenseTestBuilder(es) + + steps := func(k *test.K8sClient) test.StepList { + return test.StepList{ + test.Step{ + Name: "Create low priority StackConfigPolicy", + Test: test.Eventually(func() error { + return k.CreateOrUpdate(&lowPriorityPolicy) + }), + }, + test.Step{ + Name: "Create high priority StackConfigPolicy", + Test: test.Eventually(func() error { + return k.CreateOrUpdate(&highPriorityPolicy) + }), + }, + test.Step{ + Name: "Create non-conflicting StackConfigPolicy", + Test: test.Eventually(func() error { + return k.CreateOrUpdate(&nonConflictingPolicy) + }), + }, + test.Step{ + Name: "High priority cluster name should be applied", + Test: test.Eventually(func() error { + esClient, err := elasticsearch.NewElasticsearchClient(es.Elasticsearch, k) + if err != nil { + return err + } + + var apiResponse ClusterInfoResponse + if _, _, err = request(esClient, http.MethodGet, "/", nil, &apiResponse); err != nil { + return err + } + + if apiResponse.ClusterName != "high-priority-cluster" { + return fmt.Errorf("expected cluster name 'high-priority-cluster', got '%s'", apiResponse.ClusterName) + } + return nil + }), + }, + test.Step{ + Name: "High priority cluster settings should be applied", + Test: test.Eventually(func() error { + esClient, err := elasticsearch.NewElasticsearchClient(es.Elasticsearch, k) + if err != nil { + return err + } + + var settings ClusterSettings + _, _, err = request(esClient, http.MethodGet, "/_cluster/settings", nil, &settings) + if err != nil { + return err + } + + if settings.Persistent.Indices.Recovery.MaxBytesPerSec != "200mb" { + return fmt.Errorf("expected max_bytes_per_sec '200mb', got '%s'", settings.Persistent.Indices.Recovery.MaxBytesPerSec) + } + return nil + }), + }, + test.Step{ + Name: "Delete high priority policy - low priority should take effect", + Test: test.Eventually(func() error { + return k.Client.Delete(context.Background(), &highPriorityPolicy) + }), + }, + test.Step{ + Name: "Low priority cluster name should now be applied", + Test: test.Eventually(func() error { + esClient, err := elasticsearch.NewElasticsearchClient(es.Elasticsearch, k) + if err != nil { + return err + } + + var apiResponse ClusterInfoResponse + if _, _, err = request(esClient, http.MethodGet, "/", nil, &apiResponse); err != nil { + return err + } + + if apiResponse.ClusterName != "low-priority-cluster" { + return fmt.Errorf("expected cluster name 'low-priority-cluster', got '%s'", apiResponse.ClusterName) + } + return nil + }), + }, + test.Step{ + Name: "Low priority cluster settings should now be applied", + Test: test.Eventually(func() error { + esClient, err := elasticsearch.NewElasticsearchClient(es.Elasticsearch, k) + if err != nil { + return err + } + + var settings ClusterSettings + _, _, err = request(esClient, http.MethodGet, "/_cluster/settings", nil, &settings) + if err != nil { + return err + } + + if settings.Persistent.Indices.Recovery.MaxBytesPerSec != "50mb" { + return fmt.Errorf("expected max_bytes_per_sec '50mb', got '%s'", settings.Persistent.Indices.Recovery.MaxBytesPerSec) + } + return nil + }), + }, + test.Step{ + Name: "Clean up remaining policies", + Test: test.Eventually(func() error { + if err := k.Client.Delete(context.Background(), &lowPriorityPolicy); err != nil { + return err + } + return k.Client.Delete(context.Background(), &nonConflictingPolicy) + }), + }, + } + } + + test.Sequence(nil, steps, esWithlicense).RunSequential(t) +} + func checkAPIStatusCode(esClient client.Client, url string, expectedStatusCode int) error { var items map[string]interface{} _, actualStatusCode, _ := request(esClient, http.MethodGet, url, nil, &items) diff --git a/test/e2e/kb/stackconfigpolicy_test.go b/test/e2e/kb/stackconfigpolicy_test.go index a8a2644d84..ebf977afc1 100644 --- a/test/e2e/kb/stackconfigpolicy_test.go +++ b/test/e2e/kb/stackconfigpolicy_test.go @@ -125,3 +125,192 @@ func TestStackConfigPolicyKibana(t *testing.T) { test.Sequence(nil, steps, esWithlicense, kbBuilder).RunSequential(t) } + +// TestStackConfigPolicyKibanaMultipleWeights tests multiple StackConfigPolicies with different weights for Kibana. +func TestStackConfigPolicyKibanaMultipleWeights(t *testing.T) { + // only execute this test if we have a test license to work with + if test.Ctx().TestLicense == "" { + t.SkipNow() + } + + namespace := test.Ctx().ManagedNamespace(0) + // set up a 1-node Kibana deployment + name := "test-kb-scp-multi" + esBuilder := elasticsearch.NewBuilder(name). + WithESMasterDataNodes(1, elasticsearch.DefaultResources) + kbBuilder := kibana.NewBuilder(name). + WithElasticsearchRef(esBuilder.Ref()). + WithNodeCount(1).WithLabel("app", "kibana") + + kbPodListOpts := test.KibanaPodListOptions(kbBuilder.Kibana.Namespace, kbBuilder.Kibana.Name) + + // Policy with weight 10 (lower priority) + lowPriorityPolicy := policyv1alpha1.StackConfigPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: fmt.Sprintf("low-priority-kb-scp-%s", rand.String(4)), + }, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 10, + ResourceSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "kibana"}, + }, + Kibana: policyv1alpha1.KibanaConfigPolicySpec{ + Config: &commonv1.Config{ + Data: map[string]interface{}{ + "server.customResponseHeaders": map[string]interface{}{ + "priority": "low", + "test-header": "low-priority-value", + }, + "elasticsearch.pingTimeout": "15000", + }, + }, + SecureSettings: []commonv1.SecretSource{ + {SecretName: fmt.Sprintf("low-priority-secret-%s", rand.String(4))}, + }, + }, + }, + } + + // Policy with weight 20 (higher priority) - should override lower priority settings + highPriorityPolicy := policyv1alpha1.StackConfigPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: fmt.Sprintf("high-priority-kb-scp-%s", rand.String(4)), + }, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 20, + ResourceSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "kibana"}, + }, + Kibana: policyv1alpha1.KibanaConfigPolicySpec{ + Config: &commonv1.Config{ + Data: map[string]interface{}{ + "server.customResponseHeaders": map[string]interface{}{ + "priority": "high", + "test-header": "high-priority-value", + }, + "elasticsearch.pingTimeout": "45000", + }, + }, + SecureSettings: []commonv1.SecretSource{ + {SecretName: fmt.Sprintf("high-priority-secret-%s", rand.String(4))}, + }, + }, + }, + } + + // Policy with same weight 20 but different selector (should not conflict) + nonConflictingPolicy := policyv1alpha1.StackConfigPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: fmt.Sprintf("non-conflicting-kb-scp-%s", rand.String(4)), + }, + Spec: policyv1alpha1.StackConfigPolicySpec{ + Weight: 20, + ResourceSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "elasticsearch"}, // Different selector + }, + Kibana: policyv1alpha1.KibanaConfigPolicySpec{ + Config: &commonv1.Config{ + Data: map[string]interface{}{ + "server.customResponseHeaders": map[string]interface{}{ + "priority": "should-not-apply", + }, + }, + }, + }, + }, + } + + // Create secure settings secrets + lowPrioritySecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: lowPriorityPolicy.Spec.Kibana.SecureSettings[0].SecretName, + Namespace: namespace, + }, + Data: map[string][]byte{ + "elasticsearch.username": []byte("low_priority_user"), + }, + } + + highPrioritySecret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: highPriorityPolicy.Spec.Kibana.SecureSettings[0].SecretName, + Namespace: namespace, + }, + Data: map[string][]byte{ + "elasticsearch.username": []byte("high_priority_user"), + }, + } + + esWithlicense := test.LicenseTestBuilder(esBuilder) + + steps := func(k *test.K8sClient) test.StepList { + kibanaChecks := kibana.KbChecks{ + Client: k, + } + return test.StepList{ + test.Step{ + Name: "Create secure settings secrets", + Test: test.Eventually(func() error { + if err := k.CreateOrUpdate(&lowPrioritySecret); err != nil { + return err + } + return k.CreateOrUpdate(&highPrioritySecret) + }), + }, + test.Step{ + Name: "Create low priority StackConfigPolicy", + Test: test.Eventually(func() error { + return k.CreateOrUpdate(&lowPriorityPolicy) + }), + }, + test.Step{ + Name: "Create high priority StackConfigPolicy", + Test: test.Eventually(func() error { + return k.CreateOrUpdate(&highPriorityPolicy) + }), + }, + test.Step{ + Name: "Create non-conflicting StackConfigPolicy", + Test: test.Eventually(func() error { + return k.CreateOrUpdate(&nonConflictingPolicy) + }), + }, + // High priority settings should be applied + kibanaChecks.CheckHeaderForKey(kbBuilder, "priority", "high"), + kibanaChecks.CheckHeaderForKey(kbBuilder, "test-header", "high-priority-value"), + // High priority secure settings should be in keystore + test.CheckKeystoreEntries(k, KibanaKeystoreCmd, []string{"elasticsearch.username"}, kbPodListOpts...), + test.Step{ + Name: "Delete high priority policy - low priority should take effect", + Test: test.Eventually(func() error { + return k.Client.Delete(context.Background(), &highPriorityPolicy) + }), + }, + // Low priority settings should now be applied + kibanaChecks.CheckHeaderForKey(kbBuilder, "priority", "low"), + kibanaChecks.CheckHeaderForKey(kbBuilder, "test-header", "low-priority-value"), + // Low priority secure settings should be in keystore + test.CheckKeystoreEntries(k, KibanaKeystoreCmd, []string{"elasticsearch.username"}, kbPodListOpts...), + test.Step{ + Name: "Clean up remaining policies and secrets", + Test: test.Eventually(func() error { + if err := k.Client.Delete(context.Background(), &lowPriorityPolicy); err != nil { + return err + } + if err := k.Client.Delete(context.Background(), &nonConflictingPolicy); err != nil { + return err + } + if err := k.Client.Delete(context.Background(), &lowPrioritySecret); err != nil { + return err + } + return k.Client.Delete(context.Background(), &highPrioritySecret) + }), + }, + } + } + + test.Sequence(nil, steps, esWithlicense, kbBuilder).RunSequential(t) +} From 481391166d5ff519d62e0cc9bf82cf0dbe860ff1 Mon Sep 17 00:00:00 2001 From: tehbooom Date: Mon, 29 Sep 2025 09:15:51 -0400 Subject: [PATCH 13/14] fix: Cluster settings use canonical config --- .../filesettings/file_settings.go | 89 +++++++--- .../filesettings/file_settings_test.go | 163 +++++++++++++++++- .../stackconfigpolicy/controller_test.go | 16 +- test/e2e/es/stackconfigpolicy_test.go | 16 +- 4 files changed, 247 insertions(+), 37 deletions(-) diff --git a/pkg/controller/elasticsearch/filesettings/file_settings.go b/pkg/controller/elasticsearch/filesettings/file_settings.go index 0b43a49d2f..8f5d3653e7 100644 --- a/pkg/controller/elasticsearch/filesettings/file_settings.go +++ b/pkg/controller/elasticsearch/filesettings/file_settings.go @@ -14,6 +14,7 @@ import ( commonv1 "github.com/elastic/cloud-on-k8s/v3/pkg/apis/common/v1" policyv1alpha1 "github.com/elastic/cloud-on-k8s/v3/pkg/apis/stackconfigpolicy/v1alpha1" "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/hash" + commonsettings "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/settings" "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/version" ) @@ -127,57 +128,95 @@ func (s *Settings) updateState(es types.NamespacedName, policy policyv1alpha1.St } p.Spec.Elasticsearch.SnapshotRepositories.Data[name] = repoSettings } - s.mergeConfig(s.State.SnapshotRepositories, p.Spec.Elasticsearch.SnapshotRepositories) + s.State.SnapshotRepositories = mergeConfig(s.State.SnapshotRepositories, p.Spec.Elasticsearch.SnapshotRepositories) } - // merge other settings if p.Spec.Elasticsearch.ClusterSettings != nil { - s.mergeConfig(s.State.ClusterSettings, p.Spec.Elasticsearch.ClusterSettings) + s.State.ClusterSettings = mergeClusterConfig(s.State.ClusterSettings, p.Spec.Elasticsearch.ClusterSettings) } if p.Spec.Elasticsearch.SnapshotLifecyclePolicies != nil { - s.mergeConfig(s.State.SLM, p.Spec.Elasticsearch.SnapshotLifecyclePolicies) + s.State.SLM = mergeConfig(s.State.SLM, p.Spec.Elasticsearch.SnapshotLifecyclePolicies) } if p.Spec.Elasticsearch.SecurityRoleMappings != nil { - s.mergeConfig(s.State.RoleMappings, p.Spec.Elasticsearch.SecurityRoleMappings) + s.State.RoleMappings = mergeConfig(s.State.RoleMappings, p.Spec.Elasticsearch.SecurityRoleMappings) } if p.Spec.Elasticsearch.IndexLifecyclePolicies != nil { - s.mergeConfig(s.State.IndexLifecyclePolicies, p.Spec.Elasticsearch.IndexLifecyclePolicies) + s.State.IndexLifecyclePolicies = mergeConfig(s.State.IndexLifecyclePolicies, p.Spec.Elasticsearch.IndexLifecyclePolicies) } if p.Spec.Elasticsearch.IngestPipelines != nil { - s.mergeConfig(s.State.IngestPipelines, p.Spec.Elasticsearch.IngestPipelines) + s.State.IngestPipelines = mergeConfig(s.State.IngestPipelines, p.Spec.Elasticsearch.IngestPipelines) } if p.Spec.Elasticsearch.IndexTemplates.ComposableIndexTemplates != nil { - s.mergeConfig(s.State.IndexTemplates.ComposableIndexTemplates, p.Spec.Elasticsearch.IndexTemplates.ComposableIndexTemplates) + s.State.IndexTemplates.ComposableIndexTemplates = mergeConfig(s.State.IndexTemplates.ComposableIndexTemplates, p.Spec.Elasticsearch.IndexTemplates.ComposableIndexTemplates) } if p.Spec.Elasticsearch.IndexTemplates.ComponentTemplates != nil { - s.mergeConfig(s.State.IndexTemplates.ComponentTemplates, p.Spec.Elasticsearch.IndexTemplates.ComponentTemplates) + s.State.IndexTemplates.ComponentTemplates = mergeConfig(s.State.IndexTemplates.ComponentTemplates, p.Spec.Elasticsearch.IndexTemplates.ComponentTemplates) } return nil } -// mergeConfig merges source config into target config, with source taking precedence -// For map-type values (like snapshot repositories), individual entries are merged rather than replaced -func (s *Settings) mergeConfig(target, source *commonv1.Config) { +// mergeClusterConfig merges source config into target config with flat/nested syntax support. +// Both flat syntax (e.g., "cluster.routing.allocation.enable") and nested syntax +// (e.g., {"cluster": {"routing": {"allocation": {"enable": "value"}}}}) are supported. +// All settings are normalized to nested format for consistent output. +func mergeClusterConfig(target, source *commonv1.Config) *commonv1.Config { if source == nil || source.Data == nil { - return + return target } if target == nil || target.Data == nil { target = &commonv1.Config{Data: make(map[string]interface{})} } + // Convert to CanonicalConfig for proper dot notation handling + targetCanonical, err := commonsettings.NewCanonicalConfigFrom(target.Data) + if err != nil { + return target + } + + sourceCanonical, err := commonsettings.NewCanonicalConfigFrom(source.Data) + if err != nil { + return target + } + + // Merge with source taking precedence + err = targetCanonical.MergeWith(sourceCanonical) + if err != nil { + return target + } + + // Convert back to commonv1.Config + var result map[string]interface{} + err = targetCanonical.Unpack(&result) + if err != nil { + return target + } + + return &commonv1.Config{Data: result} +} + +// mergeConfig merges source config into target config for non-cluster settings. +// This is a simple merge without flat/nested syntax support since only ClusterSettings +// support dot notation in Elasticsearch. +func mergeConfig(target, source *commonv1.Config) *commonv1.Config { + if source == nil || source.Data == nil { + return target + } + if target == nil || target.Data == nil { + target = &commonv1.Config{Data: make(map[string]interface{})} + } + + result := &commonv1.Config{Data: make(map[string]interface{})} + + // Copy target data + for key, value := range target.Data { + result.Data[key] = value + } + + // Merge source data, with source taking precedence for key, value := range source.Data { - if targetValue, exists := target.Data[key]; exists { - if targetMap, targetIsMap := targetValue.(map[string]interface{}); targetIsMap { - if sourceMap, sourceIsMap := value.(map[string]interface{}); sourceIsMap { - for subKey, subValue := range sourceMap { - targetMap[subKey] = subValue - } - continue - } - } - } - // For non-map values or if target doesn't exist, replace entirely - target.Data[key] = value + result.Data[key] = value } + + return result } // mutateSnapshotRepositorySettings ensures that a snapshot repository can be used across multiple ES clusters. diff --git a/pkg/controller/elasticsearch/filesettings/file_settings_test.go b/pkg/controller/elasticsearch/filesettings/file_settings_test.go index 4c0b9a862e..0ddfa6a0f3 100644 --- a/pkg/controller/elasticsearch/filesettings/file_settings_test.go +++ b/pkg/controller/elasticsearch/filesettings/file_settings_test.go @@ -405,7 +405,7 @@ func Test_updateState(t *testing.T) { wantErr: errors.New("invalid type (float64) for snapshot repository path"), }, { - name: "other settings: no mutation", + name: "other settings: configuration normalization", args: args{policy: policyv1alpha1.StackConfigPolicy{Spec: policyv1alpha1.StackConfigPolicySpec{Elasticsearch: policyv1alpha1.ElasticsearchConfigPolicySpec{ ClusterSettings: clusterSettings, SnapshotRepositories: &commonv1.Config{Data: map[string]any{}}, @@ -419,7 +419,13 @@ func Test_updateState(t *testing.T) { }, }}}}, want: SettingsState{ - ClusterSettings: clusterSettings, + ClusterSettings: &commonv1.Config{Data: map[string]any{ + "indices": map[string]any{ + "recovery": map[string]any{ + "max_bytes_per_sec": "100mb", + }, + }, + }}, SnapshotRepositories: &commonv1.Config{Data: map[string]any{}}, SLM: snapshotLifecyclePolicies, RoleMappings: roleMappings, @@ -449,3 +455,156 @@ func Test_updateState(t *testing.T) { }) } } + +// TestMergeConfigWithNormalization tests the mergeClusterConfig function with various flat and nested syntax scenarios +func TestMergeConfigWithNormalization(t *testing.T) { + tests := []struct { + name string + target *commonv1.Config + source *commonv1.Config + expected *commonv1.Config + }{ + { + name: "flat syntax merged with nested syntax", + target: &commonv1.Config{Data: map[string]interface{}{ + "cluster.routing.allocation.enable": "all", + }}, + source: &commonv1.Config{Data: map[string]interface{}{ + "cluster": map[string]interface{}{ + "routing": map[string]interface{}{ + "allocation": map[string]interface{}{ + "disk": map[string]interface{}{ + "watermark": map[string]interface{}{ + "low": "85%", + }, + }, + }, + }, + }, + }}, + expected: &commonv1.Config{Data: map[string]interface{}{ + "cluster": map[string]interface{}{ + "routing": map[string]interface{}{ + "allocation": map[string]interface{}{ + "enable": "all", + "disk": map[string]interface{}{ + "watermark": map[string]interface{}{ + "low": "85%", + }, + }, + }, + }, + }, + }}, + }, + { + name: "nested syntax merged with flat syntax", + target: &commonv1.Config{Data: map[string]interface{}{ + "cluster": map[string]interface{}{ + "routing": map[string]interface{}{ + "allocation": map[string]interface{}{ + "enable": "all", + }, + }, + }, + }}, + source: &commonv1.Config{Data: map[string]interface{}{ + "cluster.routing.allocation.disk.watermark.low": "85%", + }}, + expected: &commonv1.Config{Data: map[string]interface{}{ + "cluster": map[string]interface{}{ + "routing": map[string]interface{}{ + "allocation": map[string]interface{}{ + "enable": "all", + "disk": map[string]interface{}{ + "watermark": map[string]interface{}{ + "low": "85%", + }, + }, + }, + }, + }, + }}, + }, + { + name: "flat syntax conflict - source takes precedence", + target: &commonv1.Config{Data: map[string]interface{}{ + "cluster.routing.allocation.enable": "all", + }}, + source: &commonv1.Config{Data: map[string]interface{}{ + "cluster.routing.allocation.enable": "primaries", + }}, + expected: &commonv1.Config{Data: map[string]interface{}{ + "cluster": map[string]interface{}{ + "routing": map[string]interface{}{ + "allocation": map[string]interface{}{ + "enable": "primaries", + }, + }, + }, + }}, + }, + { + name: "mixed flat and nested with different paths", + target: &commonv1.Config{Data: map[string]interface{}{ + "indices.recovery.max_bytes_per_sec": "100mb", + "cluster": map[string]interface{}{ + "routing": map[string]interface{}{ + "allocation": map[string]interface{}{ + "enable": "all", + }, + }, + }, + }}, + source: &commonv1.Config{Data: map[string]interface{}{ + "indices.memory.index_buffer_size": "10%", + "cluster.routing.rebalance.enable": "replicas", + }}, + expected: &commonv1.Config{Data: map[string]interface{}{ + "cluster": map[string]interface{}{ + "routing": map[string]interface{}{ + "allocation": map[string]interface{}{ + "enable": "all", + }, + "rebalance": map[string]interface{}{ + "enable": "replicas", + }, + }, + }, + "indices": map[string]interface{}{ + "recovery": map[string]interface{}{ + "max_bytes_per_sec": "100mb", + }, + "memory": map[string]interface{}{ + "index_buffer_size": "10%", + }, + }, + }}, + }, + { + name: "nil source returns target unchanged", + target: &commonv1.Config{Data: map[string]interface{}{"test": "value"}}, + source: nil, + expected: &commonv1.Config{Data: map[string]interface{}{ + "test": "value", + }}, + }, + { + name: "nil target returns source as target", + target: nil, + source: &commonv1.Config{Data: map[string]interface{}{"test": "value"}}, + expected: &commonv1.Config{Data: map[string]interface{}{ + "test": "value", + }}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := mergeClusterConfig(tt.target, tt.source) + if !reflect.DeepEqual(result.Data, tt.expected.Data) { + t.Errorf("mergeClusterConfig() = %+v, want %+v\nDiff: %s", result.Data, tt.expected.Data, cmp.Diff(tt.expected.Data, result.Data)) + } + }) + } +} diff --git a/pkg/controller/stackconfigpolicy/controller_test.go b/pkg/controller/stackconfigpolicy/controller_test.go index 60d37b2939..34cb9872fe 100644 --- a/pkg/controller/stackconfigpolicy/controller_test.go +++ b/pkg/controller/stackconfigpolicy/controller_test.go @@ -184,7 +184,7 @@ func TestReconcileStackConfigPolicy_Reconcile(t *testing.T) { 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":{}}}}`)}, + 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":{}}}}`)}, } secretHash, err := getSettingsHash(secretFixture) assert.NoError(t, err) @@ -447,11 +447,21 @@ func TestReconcileStackConfigPolicy_Reconcile(t *testing.T) { }, pre: func(r ReconcileStackConfigPolicy) { settings := r.getSettings(t, k8s.ExtractNamespacedName(&secretFixture)) - assert.Equal(t, "42mb", settings.State.ClusterSettings.Data["indices.recovery.max_bytes_per_sec"]) + // Check nested format in test data + indices, ok := settings.State.ClusterSettings.Data["indices"].(map[string]interface{}) + assert.True(t, ok, "indices should be a map") + recovery, ok := indices["recovery"].(map[string]interface{}) + assert.True(t, ok, "recovery should be a map") + assert.Equal(t, "42mb", recovery["max_bytes_per_sec"]) }, post: func(r ReconcileStackConfigPolicy, recorder record.FakeRecorder) { settings := r.getSettings(t, k8s.ExtractNamespacedName(&secretFixture)) - assert.Equal(t, "43mb", settings.State.ClusterSettings.Data["indices.recovery.max_bytes_per_sec"]) + // After normalization, cluster settings should be in nested format + indices, ok := settings.State.ClusterSettings.Data["indices"].(map[string]interface{}) + assert.True(t, ok, "indices should be a map") + recovery, ok := indices["recovery"].(map[string]interface{}) + assert.True(t, ok, "recovery should be a map") + assert.Equal(t, "43mb", recovery["max_bytes_per_sec"]) var policy policyv1alpha1.StackConfigPolicy err := r.Client.Get(context.Background(), types.NamespacedName{ diff --git a/test/e2e/es/stackconfigpolicy_test.go b/test/e2e/es/stackconfigpolicy_test.go index c878cdd3fd..bfac8cfa4e 100644 --- a/test/e2e/es/stackconfigpolicy_test.go +++ b/test/e2e/es/stackconfigpolicy_test.go @@ -2,8 +2,6 @@ // 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. -//go:build es || e2e - package es import ( @@ -374,8 +372,8 @@ func TestStackConfigPolicyMultipleWeights(t *testing.T) { }, ClusterSettings: &commonv1.Config{ Data: map[string]interface{}{ - "persistent": map[string]interface{}{ - "indices.recovery.max_bytes_per_sec": "50mb", + "indices": map[string]interface{}{ + "recovery.max_bytes_per_sec": "50mb", }, }, }, @@ -397,13 +395,17 @@ func TestStackConfigPolicyMultipleWeights(t *testing.T) { Elasticsearch: policyv1alpha1.ElasticsearchConfigPolicySpec{ Config: &commonv1.Config{ Data: map[string]interface{}{ - "cluster.name": "high-priority-cluster", + "cluster": map[string]interface{}{ + "name": "high-priority-cluster", + }, }, }, ClusterSettings: &commonv1.Config{ Data: map[string]interface{}{ - "persistent": map[string]interface{}{ - "indices.recovery.max_bytes_per_sec": "200mb", + "indices": map[string]interface{}{ + "recovery": map[string]interface{}{ + "max_bytes_per_sec": "200mb", + }, }, }, }, From dc6a49c76d0964e1d6eeaf441352ad2b7e7e7d7c Mon Sep 17 00:00:00 2001 From: tehbooom Date: Thu, 16 Oct 2025 13:17:18 -0400 Subject: [PATCH 14/14] fix: Flip weight priorities in scp tests --- test/e2e/es/stackconfigpolicy_test.go | 8 ++++---- test/e2e/kb/stackconfigpolicy_test.go | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/e2e/es/stackconfigpolicy_test.go b/test/e2e/es/stackconfigpolicy_test.go index bfac8cfa4e..053faa3436 100644 --- a/test/e2e/es/stackconfigpolicy_test.go +++ b/test/e2e/es/stackconfigpolicy_test.go @@ -353,14 +353,14 @@ func TestStackConfigPolicyMultipleWeights(t *testing.T) { namespace := test.Ctx().ManagedNamespace(0) - // Policy with weight 10 (lower priority) - sets cluster.name + // Policy with weight 20 (lower priority) - sets cluster.name lowPriorityPolicy := policyv1alpha1.StackConfigPolicy{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: fmt.Sprintf("low-priority-scp-%s", rand.String(4)), }, Spec: policyv1alpha1.StackConfigPolicySpec{ - Weight: 10, + Weight: 20, ResourceSelector: metav1.LabelSelector{ MatchLabels: map[string]string{"app": "elasticsearch"}, }, @@ -381,14 +381,14 @@ func TestStackConfigPolicyMultipleWeights(t *testing.T) { }, } - // Policy with weight 20 (higher priority) - should override cluster.name and settings + // Policy with weight 10 (higher priority) - should override cluster.name and settings highPriorityPolicy := policyv1alpha1.StackConfigPolicy{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: fmt.Sprintf("high-priority-scp-%s", rand.String(4)), }, Spec: policyv1alpha1.StackConfigPolicySpec{ - Weight: 20, + Weight: 10, ResourceSelector: metav1.LabelSelector{ MatchLabels: map[string]string{"app": "elasticsearch"}, }, diff --git a/test/e2e/kb/stackconfigpolicy_test.go b/test/e2e/kb/stackconfigpolicy_test.go index ebf977afc1..f64666372b 100644 --- a/test/e2e/kb/stackconfigpolicy_test.go +++ b/test/e2e/kb/stackconfigpolicy_test.go @@ -144,14 +144,14 @@ func TestStackConfigPolicyKibanaMultipleWeights(t *testing.T) { kbPodListOpts := test.KibanaPodListOptions(kbBuilder.Kibana.Namespace, kbBuilder.Kibana.Name) - // Policy with weight 10 (lower priority) + // Policy with weight 20 (lower priority) lowPriorityPolicy := policyv1alpha1.StackConfigPolicy{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: fmt.Sprintf("low-priority-kb-scp-%s", rand.String(4)), }, Spec: policyv1alpha1.StackConfigPolicySpec{ - Weight: 10, + Weight: 20, ResourceSelector: metav1.LabelSelector{ MatchLabels: map[string]string{"app": "kibana"}, }, @@ -172,14 +172,14 @@ func TestStackConfigPolicyKibanaMultipleWeights(t *testing.T) { }, } - // Policy with weight 20 (higher priority) - should override lower priority settings + // Policy with weight 10 (higher priority) - should override lower priority settings highPriorityPolicy := policyv1alpha1.StackConfigPolicy{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, Name: fmt.Sprintf("high-priority-kb-scp-%s", rand.String(4)), }, Spec: policyv1alpha1.StackConfigPolicySpec{ - Weight: 20, + Weight: 10, ResourceSelector: metav1.LabelSelector{ MatchLabels: map[string]string{"app": "kibana"}, },