Skip to content

Commit 40d5fd1

Browse files
feat: add support for multiple SCP composition
1 parent d02f4ec commit 40d5fd1

File tree

17 files changed

+1001
-390
lines changed

17 files changed

+1001
-390
lines changed

config/crds/v1/all-crds.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10932,6 +10932,13 @@ spec:
1093210932
- secretName
1093310933
type: object
1093410934
type: array
10935+
weight:
10936+
default: 0
10937+
description: |-
10938+
Weight determines the priority of this policy when multiple policies target the same resource.
10939+
Lower weight values take precedence. Defaults to 0.
10940+
format: int32
10941+
type: integer
1093510942
type: object
1093610943
status:
1093710944
properties:

config/crds/v1/resources/stackconfigpolicy.k8s.elastic.co_stackconfigpolicies.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,13 @@ spec:
288288
- secretName
289289
type: object
290290
type: array
291+
weight:
292+
default: 0
293+
description: |-
294+
Weight determines the priority of this policy when multiple policies target the same resource.
295+
Lower weight values take precedence. Defaults to 0.
296+
format: int32
297+
type: integer
291298
type: object
292299
status:
293300
properties:

deploy/eck-operator/charts/eck-operator-crds/templates/all-crds.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11002,6 +11002,13 @@ spec:
1100211002
- secretName
1100311003
type: object
1100411004
type: array
11005+
weight:
11006+
default: 0
11007+
description: |-
11008+
Weight determines the priority of this policy when multiple policies target the same resource.
11009+
Lower weight values take precedence. Defaults to 0.
11010+
format: int32
11011+
type: integer
1100511012
type: object
1100611013
status:
1100711014
properties:

docs/reference/api-reference/main.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2068,6 +2068,7 @@ StackConfigPolicy represents a StackConfigPolicy resource in a Kubernetes cluste
20682068
| Field | Description |
20692069
| --- | --- |
20702070
| *`resourceSelector`* __[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#labelselector-v1-meta)__ | |
2071+
| *`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. |
20712072
| *`secureSettings`* __[SecretSource](#secretsource) array__ | Deprecated: SecureSettings only applies to Elasticsearch and is deprecated. It must be set per application instead. |
20722073
| *`elasticsearch`* __[ElasticsearchConfigPolicySpec](#elasticsearchconfigpolicyspec)__ | |
20732074
| *`kibana`* __[KibanaConfigPolicySpec](#kibanaconfigpolicyspec)__ | |

pkg/apis/stackconfigpolicy/v1alpha1/stackconfigpolicy_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ type StackConfigPolicyList struct {
5555

5656
type StackConfigPolicySpec struct {
5757
ResourceSelector metav1.LabelSelector `json:"resourceSelector,omitempty"`
58+
// Weight determines the priority of this policy when multiple policies target the same resource.
59+
// Lower weight values take precedence. Defaults to 0.
60+
// +kubebuilder:default=0
61+
Weight int32 `json:"weight,omitempty"`
5862
// Deprecated: SecureSettings only applies to Elasticsearch and is deprecated. It must be set per application instead.
5963
SecureSettings []commonv1.SecretSource `json:"secureSettings,omitempty"`
6064
Elasticsearch ElasticsearchConfigPolicySpec `json:"elasticsearch,omitempty"`

pkg/controller/common/annotation/constants.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,6 @@ const (
2525

2626
ElasticsearchConfigAndSecretMountsHashAnnotation = "policy.k8s.elastic.co/elasticsearch-config-mounts-hash" //nolint:gosec
2727
SourceSecretAnnotationName = "policy.k8s.elastic.co/source-secret-name" //nolint:gosec
28+
29+
SoftOwnerRefsAnnotation = "eck.k8s.elastic.co/owner-refs"
2830
)

pkg/controller/common/reconciler/secret.go

Lines changed: 83 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ package reconciler
66

77
import (
88
"context"
9+
"encoding/json"
910
"reflect"
11+
"strings"
1012

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

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

98+
// SoftOwnerRefs returns the soft owner references of the given object.
99+
func SoftOwnerRefs(obj metav1.Object) ([]SoftOwnerRef, error) {
100+
// Check if this Secret has a soft-owner kind label set
101+
ownerKind, exists := obj.GetLabels()[SoftOwnerKindLabel]
102+
if !exists {
103+
// Not a soft-owned secret
104+
return nil, nil
105+
}
106+
107+
// Check for multi-policy ownership (annotation-based)
108+
if ownerRefsBytes, exists := obj.GetAnnotations()[commonannotation.SoftOwnerRefsAnnotation]; exists {
109+
// Multi-policy soft owned secret - parse the JSON map of owners
110+
var ownerRefs map[string]struct{}
111+
if err := json.Unmarshal([]byte(ownerRefsBytes), &ownerRefs); err != nil {
112+
return nil, err
113+
}
114+
115+
// Convert the map keys (namespaced name strings) back to NamespacedName objects
116+
var ownerRefsNsn []SoftOwnerRef
117+
for nsnStr := range ownerRefs {
118+
// Split the string format "namespace/name" into components
119+
nsnComponents := strings.Split(nsnStr, string(types.Separator))
120+
if len(nsnComponents) != 2 {
121+
// Skip malformed entries
122+
continue
123+
}
124+
ownerRefsNsn = append(ownerRefsNsn, SoftOwnerRef{Namespace: nsnComponents[0], Name: nsnComponents[1], Kind: ownerKind})
125+
}
126+
127+
return ownerRefsNsn, nil
128+
}
129+
130+
// Fall back to single-policy ownership (label-based)
131+
currentOwner, referenced := SoftOwnerRefFromLabels(obj.GetLabels())
132+
if !referenced {
133+
// No soft owner found in labels
134+
return nil, nil
135+
}
136+
137+
// Return the single owner as a slice with one element
138+
return []SoftOwnerRef{currentOwner}, nil
139+
}
140+
95141
// ReconcileSecretNoOwnerRef should be called to reconcile a Secret for which we explicitly don't want
96142
// an owner reference to be set, and want existing ownerReferences from previous operator versions to be removed,
97143
// because of this k8s bug: https://github.com/kubernetes/kubernetes/issues/65200 (fixed in k8s 1.20).
@@ -200,43 +246,54 @@ func GarbageCollectAllSoftOwnedOrphanSecrets(ctx context.Context, c k8s.Client,
200246
var secrets corev1.SecretList
201247
if err := c.List(ctx,
202248
&secrets,
203-
client.HasLabels{SoftOwnerNamespaceLabel, SoftOwnerNameLabel, SoftOwnerKindLabel},
249+
client.HasLabels{SoftOwnerKindLabel},
204250
); err != nil {
205251
return err
206252
}
207253
// remove any secret whose owner doesn't exist
208254
for i := range secrets.Items {
209255
secret := secrets.Items[i]
210-
softOwner, referenced := SoftOwnerRefFromLabels(secret.Labels)
211-
if !referenced {
212-
continue
213-
}
214-
if restrictedToOwnerNamespace(softOwner.Kind) && softOwner.Namespace != secret.Namespace {
215-
// Secret references an owner in a different namespace: this likely results
216-
// from a "manual" copy of the secret in another namespace, not handled by the operator.
217-
// We don't want to touch that secret.
218-
continue
256+
softOwners, err := SoftOwnerRefs(&secret)
257+
if err != nil {
258+
return err
219259
}
220-
owner, managed := ownerKinds[softOwner.Kind]
221-
if !managed {
260+
if len(softOwners) == 0 {
222261
continue
223262
}
224-
owner = k8s.DeepCopyObject(owner)
225-
err := c.Get(ctx, types.NamespacedName{Namespace: softOwner.Namespace, Name: softOwner.Name}, owner)
226-
if err != nil {
227-
if apierrors.IsNotFound(err) {
228-
// owner doesn't exit anymore
229-
ulog.FromContext(ctx).Info("Deleting secret as part of garbage collection",
230-
"namespace", secret.Namespace, "secret_name", secret.Name,
231-
"owner_kind", softOwner.Kind, "owner_namespace", softOwner.Namespace, "owner_name", softOwner.Name,
232-
)
233-
options := client.DeleteOptions{Preconditions: &metav1.Preconditions{UID: &secret.UID}}
234-
if err := c.Delete(ctx, &secret, &options); err != nil && !apierrors.IsNotFound(err) {
235-
return err
236-
}
263+
264+
missingOwners := make(map[types.NamespacedName]client.Object)
265+
for _, softOwner := range softOwners {
266+
if restrictedToOwnerNamespace(softOwner.Kind) && softOwner.Namespace != secret.Namespace {
267+
// Secret references an owner in a different namespace: this likely results
268+
// from a "manual" copy of the secret in another namespace, not handled by the operator.
269+
// We don't want to touch that secret.
237270
continue
238271
}
239-
return err
272+
owner, managed := ownerKinds[softOwner.Kind]
273+
if !managed {
274+
continue
275+
}
276+
owner = k8s.DeepCopyObject(owner)
277+
err := c.Get(ctx, types.NamespacedName{Namespace: softOwner.Namespace, Name: softOwner.Name}, owner)
278+
if err != nil {
279+
if apierrors.IsNotFound(err) {
280+
// owner doesn't exit anymore
281+
ulog.FromContext(ctx).Info("Deleting secret as part of garbage collection",
282+
"namespace", secret.Namespace, "secret_name", secret.Name,
283+
"owner_kind", softOwner.Kind, "owner_namespace", softOwner.Namespace, "owner_name", softOwner.Name,
284+
)
285+
missingOwners[types.NamespacedName{Namespace: softOwner.Namespace, Name: softOwner.Name}] = owner
286+
continue
287+
}
288+
return err
289+
}
290+
}
291+
292+
if len(missingOwners) == len(softOwners) {
293+
options := client.DeleteOptions{Preconditions: &metav1.Preconditions{UID: &secret.UID}}
294+
if err := c.Delete(ctx, &secret, &options); err != nil && !apierrors.IsNotFound(err) {
295+
return err
296+
}
240297
}
241298
// owner still exists, keep the secret
242299
}

pkg/controller/elasticsearch/filesettings/reconciler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ var (
2929

3030
// managedAnnotations are the annotations managed by the operator for the stack config policy related secrets, which means that the operator
3131
// will always take precedence to update or remove these annotations.
32-
managedAnnotations = []string{commonannotation.SecureSettingsSecretsAnnotationName, commonannotation.SettingsHashAnnotationName, commonannotation.ElasticsearchConfigAndSecretMountsHashAnnotation, commonannotation.KibanaConfigHashAnnotation}
32+
managedAnnotations = []string{commonannotation.SecureSettingsSecretsAnnotationName, commonannotation.SettingsHashAnnotationName, commonannotation.ElasticsearchConfigAndSecretMountsHashAnnotation, commonannotation.KibanaConfigHashAnnotation, commonannotation.SoftOwnerRefsAnnotation}
3333
)
3434

3535
// ReconcileEmptyFileSettingsSecret reconciles an empty File settings Secret for the given Elasticsearch only when there is no Secret.

pkg/controller/elasticsearch/filesettings/secret.go

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,6 @@ func newSettingsSecret(version int64, es types.NamespacedName, currentSecret *co
8585
}
8686

8787
if policy != nil {
88-
// set this policy as soft owner of this Secret
89-
SetSoftOwner(settingsSecret, *policy)
90-
9188
// add the Secure Settings Secret sources to the Settings Secret
9289
if err := setSecureSettings(settingsSecret, *policy); err != nil {
9390
return corev1.Secret{}, 0, err
@@ -160,19 +157,6 @@ func setSecureSettings(settingsSecret *corev1.Secret, policy policyv1alpha1.Stac
160157
return nil
161158
}
162159

163-
// CanBeOwnedBy return true if the Settings Secret can be owned by the given StackConfigPolicy, either because the Secret
164-
// belongs to no one or because it already belongs to the given policy.
165-
func CanBeOwnedBy(settingsSecret corev1.Secret, policy policyv1alpha1.StackConfigPolicy) (reconciler.SoftOwnerRef, bool) {
166-
currentOwner, referenced := reconciler.SoftOwnerRefFromLabels(settingsSecret.Labels)
167-
// either there is no soft owner
168-
if !referenced {
169-
return reconciler.SoftOwnerRef{}, true
170-
}
171-
// or the owner is already the given policy
172-
canBeOwned := currentOwner.Kind == policyv1alpha1.Kind && currentOwner.Namespace == policy.Namespace && currentOwner.Name == policy.Name
173-
return currentOwner, canBeOwned
174-
}
175-
176160
// getSecureSettings returns the SecureSettings Secret sources stores in an annotation of the given file settings Secret.
177161
func getSecureSettings(settingsSecret corev1.Secret) ([]commonv1.NamespacedSecretSource, error) {
178162
rawString, ok := settingsSecret.Annotations[commonannotation.SecureSettingsSecretsAnnotationName]

pkg/controller/elasticsearch/filesettings/secret_test.go

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -101,53 +101,6 @@ func Test_SettingsSecret_hasChanged(t *testing.T) {
101101
assert.Equal(t, strconv.FormatInt(newVersion, 10), newSettings.Metadata.Version)
102102
}
103103

104-
func Test_SettingsSecret_setSoftOwner_canBeOwnedBy(t *testing.T) {
105-
es := types.NamespacedName{
106-
Namespace: "esNs",
107-
Name: "esName",
108-
}
109-
policy := policyv1alpha1.StackConfigPolicy{
110-
TypeMeta: metav1.TypeMeta{
111-
Kind: policyv1alpha1.Kind,
112-
},
113-
ObjectMeta: metav1.ObjectMeta{
114-
Namespace: "policyNs",
115-
Name: "policyName",
116-
},
117-
}
118-
otherPolicy := policyv1alpha1.StackConfigPolicy{
119-
TypeMeta: metav1.TypeMeta{
120-
Kind: policyv1alpha1.Kind,
121-
},
122-
ObjectMeta: metav1.ObjectMeta{
123-
Namespace: "otherPolicyNs",
124-
Name: "otherPolicyName",
125-
},
126-
}
127-
128-
// empty settings can be owned by any policy
129-
secret, _, err := NewSettingsSecretWithVersion(es, nil, nil, metadata.Metadata{})
130-
assert.NoError(t, err)
131-
_, canBeOwned := CanBeOwnedBy(secret, policy)
132-
assert.Equal(t, true, canBeOwned)
133-
_, canBeOwned = CanBeOwnedBy(secret, otherPolicy)
134-
assert.Equal(t, true, canBeOwned)
135-
136-
// set a policy soft owner
137-
SetSoftOwner(&secret, policy)
138-
_, canBeOwned = CanBeOwnedBy(secret, policy)
139-
assert.Equal(t, true, canBeOwned)
140-
_, canBeOwned = CanBeOwnedBy(secret, otherPolicy)
141-
assert.Equal(t, false, canBeOwned)
142-
143-
// update the policy soft owner
144-
SetSoftOwner(&secret, otherPolicy)
145-
_, canBeOwned = CanBeOwnedBy(secret, policy)
146-
assert.Equal(t, false, canBeOwned)
147-
_, canBeOwned = CanBeOwnedBy(secret, otherPolicy)
148-
assert.Equal(t, true, canBeOwned)
149-
}
150-
151104
func Test_SettingsSecret_setSecureSettings_getSecureSettings(t *testing.T) {
152105
es := types.NamespacedName{
153106
Namespace: "esNs",

0 commit comments

Comments
 (0)