Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions config/crds/v1/all-crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10564,6 +10564,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. Defaults to 0.
format: int32
type: integer
type: object
status:
properties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. Defaults to 0.
format: int32
type: integer
type: object
status:
properties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10634,6 +10634,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. Defaults to 0.
format: int32
type: integer
type: object
status:
properties:
Expand Down
1 change: 1 addition & 0 deletions docs/reference/api-reference/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<br>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)__ | |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. Defaults to 0.
// +kubebuilder:default=0
Weight int32 `json:"weight,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cosmetic nit, I found it useful to have the weight in a column, feel free to ignore if you think otherwise:

diff --git a/pkg/apis/stackconfigpolicy/v1alpha1/stackconfigpolicy_types.go b/pkg/apis/stackconfigpolicy/v1alpha1/stackconfigpolicy_types.go
index e7d17e200..ec0de8d60 100644
--- a/pkg/apis/stackconfigpolicy/v1alpha1/stackconfigpolicy_types.go
+++ b/pkg/apis/stackconfigpolicy/v1alpha1/stackconfigpolicy_types.go
@@ -34,6 +34,7 @@ func init() {
 // +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.readyCount",description="Resources configured"
 // +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase"
 // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
+// +kubebuilder:printcolumn:name="Weight",type="integer",JSONPath=".spec.weight"
 // +kubebuilder:subresource:status
 // +kubebuilder:storageversion
 type StackConfigPolicy struct {

Result:

 k get stackconfigpolicies.stackconfigpolicy.k8s.elastic.co
NAME               READY   PHASE   AGE   WEIGHT
cluster-policy-1   1/1     Ready   20h   10
cluster-policy-2   1/1     Ready   20h   10
kibana-policy-1    1/1     Ready   20h   1
kibana-policy-2    1/1     Ready   20h   0

// 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"`
Expand Down
116 changes: 105 additions & 11 deletions pkg/controller/elasticsearch/filesettings/file_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ package filesettings
import (
"fmt"
"path/filepath"
"sort"

"k8s.io/apimachinery/pkg/types"

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"
)

Expand Down Expand Up @@ -80,10 +82,39 @@ 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)

// 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 {
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 {
Expand All @@ -97,34 +128,97 @@ func (s *Settings) updateState(es types.NamespacedName, policy policyv1alpha1.St
}
p.Spec.Elasticsearch.SnapshotRepositories.Data[name] = repoSettings
}
state.SnapshotRepositories = p.Spec.Elasticsearch.SnapshotRepositories
s.State.SnapshotRepositories = mergeConfig(s.State.SnapshotRepositories, p.Spec.Elasticsearch.SnapshotRepositories)
}
// just copy other settings
if p.Spec.Elasticsearch.ClusterSettings != nil {
state.ClusterSettings = p.Spec.Elasticsearch.ClusterSettings
s.State.ClusterSettings = mergeClusterConfig(s.State.ClusterSettings, p.Spec.Elasticsearch.ClusterSettings)
}
if p.Spec.Elasticsearch.SnapshotLifecyclePolicies != nil {
state.SLM = p.Spec.Elasticsearch.SnapshotLifecyclePolicies
s.State.SLM = mergeConfig(s.State.SLM, p.Spec.Elasticsearch.SnapshotLifecyclePolicies)
}
if p.Spec.Elasticsearch.SecurityRoleMappings != nil {
state.RoleMappings = p.Spec.Elasticsearch.SecurityRoleMappings
s.State.RoleMappings = mergeConfig(s.State.RoleMappings, p.Spec.Elasticsearch.SecurityRoleMappings)
}
if p.Spec.Elasticsearch.IndexLifecyclePolicies != nil {
state.IndexLifecyclePolicies = p.Spec.Elasticsearch.IndexLifecyclePolicies
s.State.IndexLifecyclePolicies = mergeConfig(s.State.IndexLifecyclePolicies, p.Spec.Elasticsearch.IndexLifecyclePolicies)
}
if p.Spec.Elasticsearch.IngestPipelines != nil {
state.IngestPipelines = p.Spec.Elasticsearch.IngestPipelines
s.State.IngestPipelines = mergeConfig(s.State.IngestPipelines, p.Spec.Elasticsearch.IngestPipelines)
}
if p.Spec.Elasticsearch.IndexTemplates.ComposableIndexTemplates != nil {
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 {
state.IndexTemplates.ComponentTemplates = p.Spec.Elasticsearch.IndexTemplates.ComponentTemplates
s.State.IndexTemplates.ComponentTemplates = mergeConfig(s.State.IndexTemplates.ComponentTemplates, p.Spec.Elasticsearch.IndexTemplates.ComponentTemplates)
}
s.State = state
return nil
}

// 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 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 {
result.Data[key] = value
}

return result
}

// 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/<namespace>-<esName>`
Expand Down
Loading