Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6bba692
feat: add support for multiple SCP composition
pkoutsovasilis Nov 21, 2025
400b02d
ci: add unit-tests
pkoutsovasilis Nov 21, 2025
dd18a2d
fix: move SoftOwnerRefsAnnotation from commonannotations to reconcile…
pkoutsovasilis Nov 25, 2025
b5eaad2
refactor: config policy merging and use namespaced secret sources for…
pkoutsovasilis Nov 25, 2025
994a33e
feat: add scp weight as printable column
pkoutsovasilis Nov 25, 2025
4004fc7
fix: reduce the scope of err vars
pkoutsovasilis Nov 27, 2025
5e03e79
fix: improve code readability in mergeElasticsearchSpecs func
pkoutsovasilis Nov 27, 2025
0b83045
fix: change SoftOwnerRefsAnnotation annotation value from map to list
pkoutsovasilis Nov 27, 2025
5c216cc
doc: add comment for file-settings secret soft owners
pkoutsovasilis Nov 27, 2025
48f510a
fix: reconciler unit-tests
pkoutsovasilis Nov 27, 2025
cfd5cb1
fix: rework secure-settings getter funcs
pkoutsovasilis Nov 27, 2025
6c4e575
fix: update main.md
pkoutsovasilis Nov 27, 2025
0f5d39b
Merge remote-tracking branch 'origin/main' into feature/multi-scp-com…
pkoutsovasilis Nov 28, 2025
3bf7a08
fix: rework secret mounts and sources merging
pkoutsovasilis Dec 1, 2025
799744b
fix: rework config merging
pkoutsovasilis Dec 1, 2025
025c76f
fix: relocate soft owners related funcs
pkoutsovasilis Dec 1, 2025
e3639c5
fix: reword secret mounts merge conflict errs
pkoutsovasilis Dec 1, 2025
ad6e3b1
fix: include the deprecated field of secure settings in merging
pkoutsovasilis Dec 1, 2025
8406a6c
fix: make linter happy
pkoutsovasilis Dec 1, 2025
e199c44
ci: add e2e tests
pkoutsovasilis Dec 1, 2025
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 @@ -10932,6 +10932,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 @@ -11002,6 +11002,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 @@ -2068,6 +2068,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"`
// 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
2 changes: 2 additions & 0 deletions pkg/controller/common/annotation/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ const (

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"
)
109 changes: 83 additions & 26 deletions pkg/controller/common/reconciler/secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ package reconciler

import (
"context"
"encoding/json"
"reflect"
"strings"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
Expand All @@ -17,6 +19,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"

policyv1alpha1 "github.com/elastic/cloud-on-k8s/v3/pkg/apis/stackconfigpolicy/v1alpha1"
commonannotation "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/annotation"
"github.com/elastic/cloud-on-k8s/v3/pkg/utils/k8s"
ulog "github.com/elastic/cloud-on-k8s/v3/pkg/utils/log"
"github.com/elastic/cloud-on-k8s/v3/pkg/utils/maps"
Expand Down Expand Up @@ -92,6 +95,49 @@ func SoftOwnerRefFromLabels(labels map[string]string) (SoftOwnerRef, bool) {
return SoftOwnerRef{Namespace: namespace, Name: name, Kind: kind}, true
}

// SoftOwnerRefs returns the soft owner references of the given object.
func SoftOwnerRefs(obj metav1.Object) ([]SoftOwnerRef, error) {
// Check if this Secret has a soft-owner kind label set
ownerKind, exists := obj.GetLabels()[SoftOwnerKindLabel]
if !exists {
// Not a soft-owned secret
return nil, nil
}

// Check for multi-policy ownership (annotation-based)
if ownerRefsBytes, exists := obj.GetAnnotations()[commonannotation.SoftOwnerRefsAnnotation]; exists {
// Multi-policy soft owned secret - parse the JSON map of owners
var ownerRefs map[string]struct{}
if err := json.Unmarshal([]byte(ownerRefsBytes), &ownerRefs); err != nil {
return nil, err
}

// Convert the map keys (namespaced name strings) back to NamespacedName objects
var ownerRefsNsn []SoftOwnerRef
for nsnStr := range ownerRefs {
// Split the string format "namespace/name" into components
nsnComponents := strings.Split(nsnStr, string(types.Separator))
if len(nsnComponents) != 2 {
// Skip malformed entries
continue
}
ownerRefsNsn = append(ownerRefsNsn, SoftOwnerRef{Namespace: nsnComponents[0], Name: nsnComponents[1], Kind: ownerKind})
}

return ownerRefsNsn, nil
}

// Fall back to single-policy ownership (label-based)
currentOwner, referenced := SoftOwnerRefFromLabels(obj.GetLabels())
if !referenced {
// No soft owner found in labels
return nil, nil
}

// Return the single owner as a slice with one element
return []SoftOwnerRef{currentOwner}, nil
}

// ReconcileSecretNoOwnerRef should be called to reconcile a Secret for which we explicitly don't want
// an owner reference to be set, and want existing ownerReferences from previous operator versions to be removed,
// because of this k8s bug: https://github.com/kubernetes/kubernetes/issues/65200 (fixed in k8s 1.20).
Expand Down Expand Up @@ -200,43 +246,54 @@ func GarbageCollectAllSoftOwnedOrphanSecrets(ctx context.Context, c k8s.Client,
var secrets corev1.SecretList
if err := c.List(ctx,
&secrets,
client.HasLabels{SoftOwnerNamespaceLabel, SoftOwnerNameLabel, SoftOwnerKindLabel},
client.HasLabels{SoftOwnerKindLabel},
); err != nil {
return err
}
// remove any secret whose owner doesn't exist
for i := range secrets.Items {
secret := secrets.Items[i]
softOwner, referenced := SoftOwnerRefFromLabels(secret.Labels)
if !referenced {
continue
}
if restrictedToOwnerNamespace(softOwner.Kind) && softOwner.Namespace != secret.Namespace {
// Secret references an owner in a different namespace: this likely results
// from a "manual" copy of the secret in another namespace, not handled by the operator.
// We don't want to touch that secret.
continue
softOwners, err := SoftOwnerRefs(&secret)
if err != nil {
return err
}
owner, managed := ownerKinds[softOwner.Kind]
if !managed {
if len(softOwners) == 0 {
continue
}
owner = k8s.DeepCopyObject(owner)
err := c.Get(ctx, types.NamespacedName{Namespace: softOwner.Namespace, Name: softOwner.Name}, owner)
if err != nil {
if apierrors.IsNotFound(err) {
// owner doesn't exit anymore
ulog.FromContext(ctx).Info("Deleting secret as part of garbage collection",
"namespace", secret.Namespace, "secret_name", secret.Name,
"owner_kind", softOwner.Kind, "owner_namespace", softOwner.Namespace, "owner_name", softOwner.Name,
)
options := client.DeleteOptions{Preconditions: &metav1.Preconditions{UID: &secret.UID}}
if err := c.Delete(ctx, &secret, &options); err != nil && !apierrors.IsNotFound(err) {
return err
}

missingOwners := make(map[types.NamespacedName]client.Object)
for _, softOwner := range softOwners {
if restrictedToOwnerNamespace(softOwner.Kind) && softOwner.Namespace != secret.Namespace {
// Secret references an owner in a different namespace: this likely results
// from a "manual" copy of the secret in another namespace, not handled by the operator.
// We don't want to touch that secret.
continue
}
return err
owner, managed := ownerKinds[softOwner.Kind]
if !managed {
continue
}
owner = k8s.DeepCopyObject(owner)
err := c.Get(ctx, types.NamespacedName{Namespace: softOwner.Namespace, Name: softOwner.Name}, owner)
if err != nil {
if apierrors.IsNotFound(err) {
// owner doesn't exit anymore
ulog.FromContext(ctx).Info("Deleting secret as part of garbage collection",
"namespace", secret.Namespace, "secret_name", secret.Name,
"owner_kind", softOwner.Kind, "owner_namespace", softOwner.Namespace, "owner_name", softOwner.Name,
)
missingOwners[types.NamespacedName{Namespace: softOwner.Namespace, Name: softOwner.Name}] = owner
continue
}
return err
}
}

if len(missingOwners) == len(softOwners) {
options := client.DeleteOptions{Preconditions: &metav1.Preconditions{UID: &secret.UID}}
if err := c.Delete(ctx, &secret, &options); err != nil && !apierrors.IsNotFound(err) {
return err
}
}
// owner still exists, keep the secret
}
Expand Down
Loading