-
Notifications
You must be signed in to change notification settings - Fork 775
feat: multi SCP composition #8917
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: multi SCP composition #8917
Conversation
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
🔍 Preview links for changed docs |
1ff62b3 to
c442adf
Compare
f5ee58f to
6353c66
Compare
6353c66 to
2c4aa4b
Compare
2c4aa4b to
400b02d
Compare
|
Just a general question/comment that came to my mind, do we want to have any limits on the number of SCPs that can be associated to a cluster ? |
| esPolicy.PoliciesWithConflictErrors[policyNsn] = err | ||
|
|
||
| if previouslyAppliedPolicy != nil { | ||
| previouslyAppliedPolicyNsn := k8s.ExtractNamespacedName(previouslyAppliedPolicy) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand why we use previouslyAppliedPolicy here? The conflict could come from another policy applied before?
For example, assuming we have 3 policies (p1,p2,p3), processed in that order. Let's also assume p1 conflicts with p3 then IIUC we are going to report a conflict between p1 and p2? While the conflict is actually between p1 and p3?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like I should have reloaded the PR before posting my comments: #8917 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep you are both right, this conflict does not originate only from the previously applied policy
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed this in b5eaad2 and I believe now that the conflicting policies are captured correctly
| // PoliciesWithConflictErrors maps policy namespaced names to conflict errors when multiple policies | ||
| // have the same weight | ||
| PoliciesWithConflictErrors map[types.NamespacedName]error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it only because they have the same weight? My understanding is that mergeElasticsearchConfig can also lead to policies being recorded here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the secretName collision came afterwards and I didn't update the godoc to include this reason as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have simplified the conflict handling logic in b5eaad2
| if esPolicyConfigFinal == nil { | ||
| continue | ||
| } | ||
| conflictErr, exists := esPolicyConfigFinal.PoliciesWithConflictErrors[reconcilingPolicyNsn] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's maybe add a comment to explain why using only err is not enough, and why we need to get conflictErr from esPolicyConfigFinal.PoliciesWithConflictErrors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed the behaviour in b5eaad2 and now I am using only err
pebrc
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a first pass, just looking at the code. I have not tested it yet. Will try to find some more time later today.
| ElasticsearchConfigAndSecretMountsHashAnnotation = "policy.k8s.elastic.co/elasticsearch-config-mounts-hash" //nolint:gosec | ||
| SourceSecretAnnotationName = "policy.k8s.elastic.co/source-secret-name" //nolint:gosec | ||
|
|
||
| SoftOwnerRefsAnnotation = "eck.k8s.elastic.co/owner-refs" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we keep it close to the existing labels used for soft owner refs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
moved this closer to the existing labels here dd18a2d
| // Process policies in order of weight (lowest first) | ||
| slices.Sort(weights) | ||
|
|
||
| // Reverse the weights so that we process policies in order of weight (highest first) | ||
| slices.Reverse(weights) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Process policies in order of weight (highest first)
slices.SortFunc(weights, func(a, b int32) int {
return cmp.Compare(b, a) // Reversed comparison for descending order
})Yours is kind of easier to read ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed in b5eaad2
| // have the same weight | ||
| PoliciesWithConflictErrors map[types.NamespacedName]error | ||
| // PoliciesRefs contains all StackConfigPolicies that target this Kibana instance | ||
| PoliciesRefs []policyv1alpha1.StackConfigPolicy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| PoliciesRefs []policyv1alpha1.StackConfigPolicy | |
| PolicyRefs []policyv1alpha1.StackConfigPolicy |
nit: double plural
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed in b5eaad2
| // have the same weight | ||
| PoliciesWithConflictErrors map[types.NamespacedName]error | ||
| // PoliciesRefs contains all StackConfigPolicies that target this Elasticsearch cluster | ||
| PoliciesRefs []policyv1alpha1.StackConfigPolicy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| PoliciesRefs []policyv1alpha1.StackConfigPolicy | |
| PolicyRefs []policyv1alpha1.StackConfigPolicy |
nit: double plural
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed in b5eaad2
| // doesPolicyRefsObject checks if the given StackConfigPolicy targets the given Elasticsearch cluster. | ||
| // A policy targets an Elasticsearch cluster if both following conditions are met: | ||
| // 1. The policy is in either the operator namespace or the same namespace as the Elasticsearch cluster | ||
| // 2. The policy's label selector matches the Elasticsearch cluster's labels | ||
| // Returns true or false depending on whether the given policy targets the Elasticsearch cluster and | ||
| // an error if the label selector is invalid. | ||
| func doesPolicyRefsObject(policy *policyv1alpha1.StackConfigPolicy, obj metav1.Object, operatorNamespace string) (bool, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // doesPolicyRefsObject checks if the given StackConfigPolicy targets the given Elasticsearch cluster. | |
| // A policy targets an Elasticsearch cluster if both following conditions are met: | |
| // 1. The policy is in either the operator namespace or the same namespace as the Elasticsearch cluster | |
| // 2. The policy's label selector matches the Elasticsearch cluster's labels | |
| // Returns true or false depending on whether the given policy targets the Elasticsearch cluster and | |
| // an error if the label selector is invalid. | |
| func doesPolicyRefsObject(policy *policyv1alpha1.StackConfigPolicy, obj metav1.Object, operatorNamespace string) (bool, error) { | |
| // doesPolicyTargetObject checks if the given StackConfigPolicy targets the given object | |
| // A policy targets an object if both following conditions are met: | |
| // 1. The policy is in either the operator namespace or the same namespace as the object | |
| // 2. The policy's label selector matches the objects's labels | |
| // Returns true or false depending on whether the given policy targets the object and | |
| // an error if the label selector is invalid. | |
| func doesPolicyTargetObject(policy *policyv1alpha1.StackConfigPolicy, obj metav1.Object, operatorNamespace string) (bool, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or alternatively: doesPolicyMatchObject
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gonna go with doesPolicyMatchObject
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed in b5eaad2
| // Add all destination entries | ||
| for _, secureSetting := range dst { | ||
| secureSettings[secureSetting.SecretName] = make(map[string]commonv1.KeyToPath) | ||
| for _, entry := range secureSetting.Entries { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Entries is optional. If not specified all entries of the secret should be used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed in b5eaad2, no more special handling Secret sources, these are just getting merged as they come
|
|
||
| // Group policies by weight | ||
| var weights []int32 | ||
| weightKeyStackPolicies := make(map[int32][]*policyv1alpha1.StackConfigPolicy) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| weightKeyStackPolicies := make(map[int32][]*policyv1alpha1.StackConfigPolicy) | |
| policiesByWeight := make(map[int32][]*policyv1alpha1.StackConfigPolicy) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed in b5eaad2
| // create the expected Settings Secret | ||
| expectedSecret, expectedVersion, err := filesettings.NewSettingsSecretWithVersion(esNsn, &actualSettingsSecret, &policy, meta) | ||
| expectedSecret, expectedVersion, err := filesettings.NewSettingsSecretWithVersion(esNsn, &actualSettingsSecret, &policyv1alpha1.StackConfigPolicy{ | ||
| ObjectMeta: reconcilingPolicy.ObjectMeta, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this not introduce a bug in that the secure settings sources will always be looked for in the namespace of reconciling policy? It needs to be specific to the source policy doesn't it? Also as we can have mulitple polices being reconciled will we not have an edit war over the namespace of the secret source?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes you are right this is a bug, I did miss the fact that we use also the policy namespace for the secure settings. thx for catching that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed this in b5eaad2 to extract namespaced secret sources during the merging of all policies
| // It processes all provided policies, filtering those that target the Elasticsearch cluster, and merges them | ||
| // in order of their weight (lowest to highest). Policies with the same weight are flagged as conflicts. | ||
| // Returns an esPolicyConfig containing the merged configuration and any error occurred during merging. | ||
| func getPolicyConfigForElasticsearch(es *esv1.Elasticsearch, allPolicies []policyv1alpha1.StackConfigPolicy, params operator.Parameters) (*esPolicyConfig, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a lot of overlap with the Kibana version of this function. We could go with a generic version:
// Generic config holder
type policyConfig[T any] struct {
Spec T
PoliciesWithConflictErrors map[types.NamespacedName]error
PoliciesRefs []policyv1alpha1.StackConfigPolicy
}
// Generic merge function
func getPolicyConfigFor[T any](
obj metav1.Object, // Both ES and KB implement this
allPolicies []policyv1alpha1.StackConfigPolicy,
params operator.Parameters,
extractSpec func(*policyv1alpha1.StackConfigPolicy) T,
mergeSpec func(*T, T) error,
) (*policyConfig[T], error) {
// ... unified implementation ...
}
// Usage:
esConfig, err := getPolicyConfigFor(
es,
allPolicies,
params,
func(p *policyv1alpha1.StackConfigPolicy) policyv1alpha1.ElasticsearchConfigPolicySpec {
return p.Spec.Elasticsearch
},
mergeElasticsearchConfig,
)
kbConfig, err := getPolicyConfigFor(
kb,
allPolicies,
params,
func(p *policyv1alpha1.StackConfigPolicy) policyv1alpha1.KibanaConfigPolicySpec {
return p.Spec.Kibana
},
mergeKibanaConfig,
)Or we could maybe at least pull the shared bits into functions used from both?
Idk something like
func groupPoliciesByWeight(allPolicies []policyv1alpha1.StackConfigPolicy, filter func(*policyv1alpha1.StackConfigPolicy) bool) ([]int32, map[int32][]*policyv1alpha1.StackConfigPolicy, []policyv1alpha1.StackConfigPolicy)
func handleWeightConflicts[T any](config *policyConfig[T], policiesWithSameWeight []*policyv1alpha1.StackConfigPolicy, weight int32) errorThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a refactor that follows the proposed generics approach in b5eaad2
| switch { | ||
| case errors.Is(err, errMergeConflict): | ||
| log.V(1).Info("StackConfigPolicy merge conflict for Elasticsearch", "es_namespace", es.Namespace, "es_name", es.Name, "error", err) | ||
| results.WithRequeue(defaultRequeue) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was wondering why do we need to requeue here. Are we not expecting the user to fix the policy? (which would trigger a new reconciliation?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you are right, the requeue here is redundant and shouldn't happen, the expectation is what you say above, so I am gonna remove it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed the redundant requeue in b5eaad2
| log.V(1).Info("StackConfigPolicy merge conflict for Elasticsearch", "es_namespace", es.Namespace, "es_name", es.Name, "error", err) | ||
| results.WithRequeue(defaultRequeue) | ||
| if esPolicyConfigFinal == nil { | ||
| continue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In what cases is this path (we have a errMergeConflict but esPolicyConfigFinal is nil) taken? And how the user can understand the above err if it is logged at the debug level?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in the current code there is no path that this combo of errMergeConflict and esPolicyConfigFinal is feasible, however esPolicyConfigFinal can be nil and should be checked, no?! Now this is a Info level log message so why you say debug?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now this is a Info level log message so why you say debug?
https://github.com/elastic/cloud-on-k8s/blob/main/CONTRIBUTING.md#logging
We only use two levels:
debugandinfo. To produce a log at thedebuglevel useV(1)before theInfocall:logger.V(1).Info("starting reconciliation", "pod", req.NamespacedNamed)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh that explains it, thanks for the schooling
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
however esPolicyConfigFinal can be nil and should be checked, no?!
Not necessarily, you can use something like:
func (es *esPolicyConfig) HasErrorForPolicy(policyNsn types.NamespacedName) error {
if es == nil {
return nil
}
if err, exists := es.PoliciesWithConflictErrors[policyNsn]; exists {
return err
}
return nil
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok what you propose still does the nil check but it confines everything in a esPolicyConfig struct func, which improves the code readability so more than happy to go with it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I simplified the merge conflict err handling in b5eaad2 so this is no longer necessary
390915d to
98b9439
Compare
… secure settings related secrets
98b9439 to
994a33e
Compare
| // GetElasticsearchNamespacedSecureSettings returns the Elasticsearch secure settings from this policy | ||
| // as NamespacedSecretSources, with each secret source namespaced to the policy's namespace. | ||
| // Returns nil if the policy is nil or has no Elasticsearch secure settings defined. | ||
| func (p *StackConfigPolicy) GetElasticsearchNamespacedSecureSettings() []commonv1.NamespacedSecretSource { | ||
| if p == nil { | ||
| return nil | ||
| } | ||
|
|
||
| ssLen := len(p.Spec.Elasticsearch.SecureSettings) | ||
| if ssLen == 0 { | ||
| return nil | ||
| } | ||
| pNs := p.GetNamespace() | ||
| ssNsn := make([]commonv1.NamespacedSecretSource, ssLen) | ||
| for idx, ss := range p.Spec.Elasticsearch.SecureSettings { | ||
| ssNsn[idx] = commonv1.NamespacedSecretSource{ | ||
| Namespace: pNs, | ||
| SecretName: ss.SecretName, | ||
| Entries: ss.Entries, | ||
| } | ||
| } | ||
| return ssNsn | ||
| } | ||
|
|
||
| // GetKibanaNamespacedSecureSettings returns the Kibana secure settings from this policy | ||
| // as NamespacedSecretSources, with each secret source namespaced to the policy's namespace. | ||
| // Returns nil if the policy is nil or has no Kibana secure settings defined. | ||
| func (p *StackConfigPolicy) GetKibanaNamespacedSecureSettings() []commonv1.NamespacedSecretSource { | ||
| if p == nil { | ||
| return nil | ||
| } | ||
|
|
||
| ssLen := len(p.Spec.Kibana.SecureSettings) | ||
| if ssLen == 0 { | ||
| return nil | ||
| } | ||
| pNs := p.GetNamespace() | ||
| ssNsn := make([]commonv1.NamespacedSecretSource, ssLen) | ||
| for idx, ss := range p.Spec.Kibana.SecureSettings { | ||
| ssNsn[idx] = commonv1.NamespacedSecretSource{ | ||
| Namespace: pNs, | ||
| SecretName: ss.SecretName, | ||
| Entries: ss.Entries, | ||
| } | ||
| } | ||
| return ssNsn | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, duplicated code/logic
| // GetElasticsearchNamespacedSecureSettings returns the Elasticsearch secure settings from this policy | |
| // as NamespacedSecretSources, with each secret source namespaced to the policy's namespace. | |
| // Returns nil if the policy is nil or has no Elasticsearch secure settings defined. | |
| func (p *StackConfigPolicy) GetElasticsearchNamespacedSecureSettings() []commonv1.NamespacedSecretSource { | |
| if p == nil { | |
| return nil | |
| } | |
| ssLen := len(p.Spec.Elasticsearch.SecureSettings) | |
| if ssLen == 0 { | |
| return nil | |
| } | |
| pNs := p.GetNamespace() | |
| ssNsn := make([]commonv1.NamespacedSecretSource, ssLen) | |
| for idx, ss := range p.Spec.Elasticsearch.SecureSettings { | |
| ssNsn[idx] = commonv1.NamespacedSecretSource{ | |
| Namespace: pNs, | |
| SecretName: ss.SecretName, | |
| Entries: ss.Entries, | |
| } | |
| } | |
| return ssNsn | |
| } | |
| // GetKibanaNamespacedSecureSettings returns the Kibana secure settings from this policy | |
| // as NamespacedSecretSources, with each secret source namespaced to the policy's namespace. | |
| // Returns nil if the policy is nil or has no Kibana secure settings defined. | |
| func (p *StackConfigPolicy) GetKibanaNamespacedSecureSettings() []commonv1.NamespacedSecretSource { | |
| if p == nil { | |
| return nil | |
| } | |
| ssLen := len(p.Spec.Kibana.SecureSettings) | |
| if ssLen == 0 { | |
| return nil | |
| } | |
| pNs := p.GetNamespace() | |
| ssNsn := make([]commonv1.NamespacedSecretSource, ssLen) | |
| for idx, ss := range p.Spec.Kibana.SecureSettings { | |
| ssNsn[idx] = commonv1.NamespacedSecretSource{ | |
| Namespace: pNs, | |
| SecretName: ss.SecretName, | |
| Entries: ss.Entries, | |
| } | |
| } | |
| return ssNsn | |
| } | |
| // GetElasticsearchNamespacedSecureSettings returns the Elasticsearch secure settings from this policy | |
| // as NamespacedSecretSources, with each secret source namespaced to the policy's namespace. | |
| // Returns nil if the policy is nil or has no Elasticsearch secure settings defined. | |
| func (p *StackConfigPolicy) GetElasticsearchNamespacedSecureSettings() []commonv1.NamespacedSecretSource { | |
| if p == nil { | |
| return nil | |
| } | |
| return toNamespacedSecretSources(&p.Spec.Elasticsearch, p.Namespace) | |
| } | |
| // GetKibanaNamespacedSecureSettings returns the Kibana secure settings from this policy | |
| // as NamespacedSecretSources, with each secret source namespaced to the policy's namespace. | |
| // Returns nil if the policy is nil or has no Kibana secure settings defined. | |
| func (p *StackConfigPolicy) GetKibanaNamespacedSecureSettings() []commonv1.NamespacedSecretSource { | |
| if p == nil { | |
| return nil | |
| } | |
| return toNamespacedSecretSources(&p.Spec.Kibana, p.Namespace) | |
| } | |
| // HasSecureSettings represents a ConfigPolicySpec that has secure settings. | |
| type HasSecureSettings interface { | |
| GetSecureSettings() []commonv1.SecretSource | |
| } | |
| func toNamespacedSecretSources(hasSecureSettings HasSecureSettings, inNamespace string) []commonv1.NamespacedSecretSource { | |
| secureSettings := hasSecureSettings.GetSecureSettings() | |
| namespacedSecretSources := make([]commonv1.NamespacedSecretSource, len(secureSettings)) | |
| for i, s := range secureSettings { | |
| namespacedSecretSources[i] = commonv1.NamespacedSecretSource{ | |
| Namespace: inNamespace, | |
| SecretName: s.SecretName, | |
| Entries: s.Entries, | |
| } | |
| } | |
| return namespacedSecretSources | |
| } | |
| func (e *ElasticsearchConfigPolicySpec) GetSecureSettings() []commonv1.SecretSource { | |
| return e.SecureSettings | |
| } | |
| func (k *KibanaConfigPolicySpec) GetSecureSettings() []commonv1.SecretSource { | |
| return k.SecureSettings | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep more than a valid point thanks for the proposal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to add // +kubebuilder:object:generate=false before the interface, otherwise controller-gen is not happy:
// HasSecureSettings represents a ConfigPolicySpec that has secure settings.
// +kubebuilder:object:generate=false
type HasSecureSettings interface {
GetSecureSettings() []commonv1.SecretSource
}
Overview
This PR implements support for multiple StackConfigPolicies (SCPs) targeting the same Elasticsearch cluster or Kibana instance, using a weight-based priority system for deterministic policy composition.
Key Features
Weight-Based Priority System
0Conflict Detection
Conflicts are detected across multiple dimensions and will prevent policy application:
SecretMountwith sameSecretNameSecretMountwith sameMountPathImportant: Even if two policies with the same weight have non-overlapping resources, they still conflict because the weight collision makes the merge order ambiguous.
Configuration Merging Behaviour
Different merge strategies are applied based on the configuration type:
Deep Merge (recursive merging):
ClusterSettingsConfigSnapshotLifecyclePoliciesSecurityRoleMappingsIndexLifecyclePoliciesIngestPipelinesIndexTemplates.ComposableIndexTemplatesIndexTemplates.ComponentTemplatesTop-Level Key Replacement (entire keys replaced):
SnapshotRepositories- each repository configuration is treated atomicallyUnion Merge (with conflict detection):
SecretMounts- conflicts on duplicateSecretNameOR duplicateMountPathSecureSettings- merges bySecretName+Key, lower weight wins (no conflicts)Multi-Soft-Owner Secret Management
File Settings and Policy Config Secrets:
eck.k8s.elastic.co/owner-refsannotation with JSON-encoded map of owner namespaced namesSecret Sources:
This prevents secret leakage while enabling proper cleanup when policies are deleted.
Related Issues