Skip to content

Commit 0e2d66d

Browse files
committed
feat: support labels/annotations on connection secrets for modern MRs and claims
Add metadata.labels and metadata.annotations to writeConnectionSecretToRef for modern (namespaced) managed resources and claims via LocalSecretReference. - LocalConnectionSecretFor clones labels/annotations from ref.Metadata - connectionSecretNeedsUpdate detects metadata drift when manageMetadata=true - Omitted metadata preserves existing secret labels/annotations (merge patch) - Set metadata declaratively reconciles (merge patch replaces full map) - Shared publishConnectionSecret helper deduplicates both publishers - XRD schema includes metadata in claim writeConnectionSecretToRef validation Closes: #1008 Signed-off-by: Florian Fl Bauer <florian.fl.bauer@deutschebahn.com>
1 parent d331401 commit 0e2d66d

6 files changed

Lines changed: 205 additions & 45 deletions

File tree

pkg/reconciler/managed/api.go

Lines changed: 30 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -106,25 +106,7 @@ func (a *APISecretPublisher) PublishConnection(ctx context.Context, o resource.C
106106
s := resource.ConnectionSecretFor(o, resource.MustGetKind(o, a.typer))
107107
s.Data = c
108108

109-
err := a.secret.Apply(ctx, s,
110-
resource.ConnectionSecretMustBeControllableBy(o.GetUID()),
111-
resource.AllowUpdateIf(func(current, desired runtime.Object) bool {
112-
// We consider the update to be a no-op and don't allow it if the
113-
// current and existing secret data are identical.
114-
//nolint:forcetypeassert // Will always be a secret.
115-
return !cmp.Equal(current.(*corev1.Secret).Data, desired.(*corev1.Secret).Data, cmpopts.EquateEmpty())
116-
}),
117-
)
118-
if resource.IsNotAllowed(err) {
119-
// The update was not allowed because it was a no-op.
120-
return false, nil
121-
}
122-
123-
if err != nil {
124-
return false, errors.Wrap(err, errCreateOrUpdateSecret)
125-
}
126-
127-
return true, nil
109+
return publishConnectionSecret(ctx, a.secret, s, o.GetUID(), false)
128110
}
129111

130112
// UnpublishConnection is no-op since PublishConnection only creates resources
@@ -164,35 +146,48 @@ func (a *APILocalSecretPublisher) PublishConnection(ctx context.Context, o resou
164146
s := resource.LocalConnectionSecretFor(o, resource.MustGetKind(o, a.typer))
165147
s.Data = c
166148

167-
err := a.secret.Apply(ctx, s,
168-
resource.ConnectionSecretMustBeControllableBy(o.GetUID()),
149+
return publishConnectionSecret(ctx, a.secret, s, o.GetUID(), o.GetWriteConnectionSecretToReference().Metadata != nil)
150+
}
151+
152+
// UnpublishConnection is no-op since PublishConnection only creates resources
153+
// that will be garbage collected by Kubernetes when the managed resource is
154+
// deleted.
155+
func (a *APILocalSecretPublisher) UnpublishConnection(_ context.Context, _ resource.LocalConnectionSecretOwner, _ ConnectionDetails) error {
156+
return nil
157+
}
158+
159+
func connectionSecretNeedsUpdate(current, desired *corev1.Secret, manageMetadata bool) bool {
160+
if !cmp.Equal(current.Data, desired.Data, cmpopts.EquateEmpty()) {
161+
return true
162+
}
163+
164+
if !manageMetadata {
165+
return false
166+
}
167+
168+
return !cmp.Equal(current.Labels, desired.Labels, cmpopts.EquateEmpty()) ||
169+
!cmp.Equal(current.Annotations, desired.Annotations, cmpopts.EquateEmpty())
170+
}
171+
172+
// publishConnectionSecret applies the desired Secret via the given applicator,
173+
// returning whether the secret was published (i.e. changed) and any error.
174+
func publishConnectionSecret(ctx context.Context, applicator resource.Applicator, s *corev1.Secret, uid types.UID, manageMetadata bool) (bool, error) {
175+
err := applicator.Apply(ctx, s,
176+
resource.ConnectionSecretMustBeControllableBy(uid),
169177
resource.AllowUpdateIf(func(current, desired runtime.Object) bool {
170-
// We consider the update to be a no-op and don't allow it if the
171-
// current and existing secret data are identical.
172178
//nolint:forcetypeassert // Will always be a secret.
173-
// NOTE(erhancagirici): cmp package is not recommended for production use
174-
return !cmp.Equal(current.(*corev1.Secret).Data, desired.(*corev1.Secret).Data, cmpopts.EquateEmpty())
179+
return connectionSecretNeedsUpdate(current.(*corev1.Secret), desired.(*corev1.Secret), manageMetadata)
175180
}),
176181
)
177182
if resource.IsNotAllowed(err) {
178-
// The update was not allowed because it was a no-op.
179183
return false, nil
180184
}
181-
182185
if err != nil {
183186
return false, errors.Wrap(err, errCreateOrUpdateSecret)
184187
}
185-
186188
return true, nil
187189
}
188190

189-
// UnpublishConnection is no-op since PublishConnection only creates resources
190-
// that will be garbage collected by Kubernetes when the managed resource is
191-
// deleted.
192-
func (a *APILocalSecretPublisher) UnpublishConnection(_ context.Context, _ resource.LocalConnectionSecretOwner, _ ConnectionDetails) error {
193-
return nil
194-
}
195-
196191
// An APISimpleReferenceResolver resolves references from one managed resource
197192
// to others by calling the referencing resource's ResolveReferences method, if
198193
// any.

pkg/reconciler/managed/api_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,10 @@ func TestAPILocalSecretPublisher(t *testing.T) {
253253
mg := &fake.ModernManaged{
254254
LocalConnectionSecretWriterTo: fake.LocalConnectionSecretWriterTo{Ref: &xpv2.LocalSecretReference{
255255
Name: "coolsecret",
256+
Metadata: &xpv2.SecretReferenceMetadata{
257+
Labels: map[string]string{"desired": "label"},
258+
Annotations: map[string]string{"desired": "annotation"},
259+
},
256260
}},
257261
}
258262

@@ -328,6 +332,65 @@ func TestAPILocalSecretPublisher(t *testing.T) {
328332
err: nil,
329333
},
330334
},
335+
"MetadataChanged": {
336+
reason: "Metadata-only changes should publish an updated local secret",
337+
fields: fields{
338+
secret: resource.ApplyFn(func(ctx context.Context, o client.Object, ao ...resource.ApplyOption) error {
339+
current := resource.LocalConnectionSecretFor(mg, fake.GVK(mg))
340+
current.Data = cd
341+
current.Labels = map[string]string{"stale": "label"}
342+
current.Annotations = map[string]string{"stale": "annotation"}
343+
344+
for _, fn := range ao {
345+
if err := fn(ctx, current, o); err != nil {
346+
return err
347+
}
348+
}
349+
350+
return nil
351+
}),
352+
typer: fake.SchemeWith(&fake.ModernManaged{}),
353+
},
354+
args: args{
355+
ctx: context.Background(),
356+
mg: mg,
357+
c: cd,
358+
},
359+
want: want{published: true},
360+
},
361+
"MetadataOmittedPreservesExisting": {
362+
reason: "Omitting metadata should preserve existing local secret metadata and remain a no-op when data matches",
363+
fields: fields{
364+
secret: resource.ApplyFn(func(ctx context.Context, o client.Object, ao ...resource.ApplyOption) error {
365+
withoutMetadata := mg.DeepCopyObject().(*fake.ModernManaged)
366+
withoutMetadata.LocalConnectionSecretWriterTo.Ref = &xpv2.LocalSecretReference{Name: "coolsecret"}
367+
want := resource.LocalConnectionSecretFor(withoutMetadata, fake.GVK(withoutMetadata))
368+
want.Data = cd
369+
want.Labels = map[string]string{"keep": "label"}
370+
want.Annotations = map[string]string{"keep": "annotation"}
371+
372+
for _, fn := range ao {
373+
if err := fn(ctx, want, o); err != nil {
374+
return err
375+
}
376+
}
377+
378+
return nil
379+
}),
380+
typer: fake.SchemeWith(&fake.ModernManaged{}),
381+
},
382+
args: args{
383+
ctx: context.Background(),
384+
mg: &fake.ModernManaged{
385+
LocalConnectionSecretWriterTo: fake.LocalConnectionSecretWriterTo{Ref: &xpv2.LocalSecretReference{Name: "coolsecret"}},
386+
},
387+
c: cd,
388+
},
389+
want: want{
390+
published: false,
391+
err: nil,
392+
},
393+
},
331394
"Success": {
332395
reason: "A successful application of the connection secret should result in no error",
333396
fields: fields{

pkg/resource/resource.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package resource
1919
import (
2020
"context"
2121
"fmt"
22+
"maps"
2223
"sort"
2324
"strings"
2425

@@ -94,10 +95,20 @@ type LocalConnectionSecretOwner interface {
9495
// LocalConnectionSecretFor creates a connection secret in the namespace of the
9596
// supplied LocalConnectionSecretOwner, assumed to be of the supplied kind.
9697
func LocalConnectionSecretFor(o LocalConnectionSecretOwner, kind schema.GroupVersionKind) *corev1.Secret {
98+
ref := o.GetWriteConnectionSecretToReference()
99+
100+
var labels, annotations map[string]string
101+
if ref.Metadata != nil {
102+
labels = maps.Clone(ref.Metadata.Labels)
103+
annotations = maps.Clone(ref.Metadata.Annotations)
104+
}
105+
97106
return &corev1.Secret{
98107
ObjectMeta: metav1.ObjectMeta{
99108
Namespace: o.GetNamespace(),
100-
Name: o.GetWriteConnectionSecretToReference().Name,
109+
Name: ref.Name,
110+
Labels: labels,
111+
Annotations: annotations,
101112
OwnerReferences: []metav1.OwnerReference{meta.AsController(meta.TypedReferenceTo(o, kind))},
102113
},
103114
Type: SecretTypeConnection,
@@ -110,10 +121,12 @@ func LocalConnectionSecretFor(o LocalConnectionSecretOwner, kind schema.GroupVer
110121
// written to 'default' namespace if the ConnectionSecretOwner does not specify
111122
// a namespace.
112123
func ConnectionSecretFor(o ConnectionSecretOwner, kind schema.GroupVersionKind) *corev1.Secret {
124+
ref := o.GetWriteConnectionSecretToReference()
125+
113126
return &corev1.Secret{
114127
ObjectMeta: metav1.ObjectMeta{
115-
Namespace: o.GetWriteConnectionSecretToReference().Namespace,
116-
Name: o.GetWriteConnectionSecretToReference().Name,
128+
Namespace: ref.Namespace,
129+
Name: ref.Name,
117130
OwnerReferences: []metav1.OwnerReference{meta.AsController(meta.TypedReferenceTo(o, kind))},
118131
},
119132
Type: SecretTypeConnection,

pkg/resource/resource_test.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,24 @@ func TestLocalConnectionSecretFor(t *testing.T) {
7878
Name: name,
7979
UID: uid,
8080
},
81-
Ref: &xpv2.LocalSecretReference{Name: secretName},
81+
Ref: &xpv2.LocalSecretReference{
82+
Name: secretName,
83+
Metadata: &xpv2.SecretReferenceMetadata{
84+
Labels: map[string]string{"l": "v"},
85+
Annotations: map[string]string{"a": "v"},
86+
},
87+
},
8288
},
8389
kind: MockOwnerGVK,
8490
},
8591
want: &corev1.Secret{
8692
ObjectMeta: metav1.ObjectMeta{
8793
Namespace: namespace,
8894
Name: secretName,
95+
Labels: map[string]string{"l": "v"},
96+
Annotations: map[string]string{
97+
"a": "v",
98+
},
8999
OwnerReferences: []metav1.OwnerReference{{
90100
APIVersion: MockOwnerGVK.GroupVersion().String(),
91101
Kind: MockOwnerGVK.Kind,
@@ -132,7 +142,10 @@ func TestConnectionSecretFor(t *testing.T) {
132142
Name: name,
133143
UID: uid,
134144
},
135-
WriterTo: &xpv2.SecretReference{Namespace: namespace, Name: secretName},
145+
WriterTo: &xpv2.SecretReference{
146+
Namespace: namespace,
147+
Name: secretName,
148+
},
136149
},
137150
kind: MockOwnerGVK,
138151
},

pkg/xcrd/crd_test.go

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2893,6 +2893,25 @@ func TestForCompositeResourceClaim(t *testing.T) {
28932893
Required: []string{"name"},
28942894
Properties: map[string]extv1.JSONSchemaProps{
28952895
"name": {Type: "string"},
2896+
"metadata": {
2897+
Type: "object",
2898+
Properties: map[string]extv1.JSONSchemaProps{
2899+
"labels": {
2900+
Type: "object",
2901+
AdditionalProperties: &extv1.JSONSchemaPropsOrBool{
2902+
Allows: true,
2903+
Schema: &extv1.JSONSchemaProps{Type: "string"},
2904+
},
2905+
},
2906+
"annotations": {
2907+
Type: "object",
2908+
AdditionalProperties: &extv1.JSONSchemaPropsOrBool{
2909+
Allows: true,
2910+
Schema: &extv1.JSONSchemaProps{Type: "string"},
2911+
},
2912+
},
2913+
},
2914+
},
28962915
},
28972916
},
28982917
},
@@ -3151,6 +3170,25 @@ func TestForCompositeResourceClaim(t *testing.T) {
31513170
Required: []string{"name"},
31523171
Properties: map[string]extv1.JSONSchemaProps{
31533172
"name": {Type: "string"},
3173+
"metadata": {
3174+
Type: "object",
3175+
Properties: map[string]extv1.JSONSchemaProps{
3176+
"labels": {
3177+
Type: "object",
3178+
AdditionalProperties: &extv1.JSONSchemaPropsOrBool{
3179+
Allows: true,
3180+
Schema: &extv1.JSONSchemaProps{Type: "string"},
3181+
},
3182+
},
3183+
"annotations": {
3184+
Type: "object",
3185+
AdditionalProperties: &extv1.JSONSchemaPropsOrBool{
3186+
Allows: true,
3187+
Schema: &extv1.JSONSchemaProps{Type: "string"},
3188+
},
3189+
},
3190+
},
3191+
},
31543192
},
31553193
},
31563194
},
@@ -3426,14 +3464,33 @@ func TestForCompositeResourceClaimEmptyXrd(t *testing.T) {
34263464
"name": {Type: "string"},
34273465
},
34283466
},
3429-
"writeConnectionSecretToRef": {
3430-
Type: "object",
3431-
Required: []string{"name"},
3432-
Properties: map[string]extv1.JSONSchemaProps{
3433-
"name": {Type: "string"},
3467+
"writeConnectionSecretToRef": {
3468+
Type: "object",
3469+
Required: []string{"name"},
3470+
Properties: map[string]extv1.JSONSchemaProps{
3471+
"name": {Type: "string"},
3472+
"metadata": {
3473+
Type: "object",
3474+
Properties: map[string]extv1.JSONSchemaProps{
3475+
"labels": {
3476+
Type: "object",
3477+
AdditionalProperties: &extv1.JSONSchemaPropsOrBool{
3478+
Allows: true,
3479+
Schema: &extv1.JSONSchemaProps{Type: "string"},
3480+
},
3481+
},
3482+
"annotations": {
3483+
Type: "object",
3484+
AdditionalProperties: &extv1.JSONSchemaPropsOrBool{
3485+
Allows: true,
3486+
Schema: &extv1.JSONSchemaProps{Type: "string"},
3487+
},
3488+
},
3489+
},
34343490
},
34353491
},
34363492
},
3493+
},
34373494
},
34383495
"status": {
34393496
Type: "object",

pkg/xcrd/schemas.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,25 @@ func CompositeResourceClaimSpecProps(defaultPol *xpv2.CompositeDeletePolicy) map
283283
Required: []string{"name"},
284284
Properties: map[string]extv1.JSONSchemaProps{
285285
"name": {Type: "string"},
286+
"metadata": {
287+
Type: "object",
288+
Properties: map[string]extv1.JSONSchemaProps{
289+
"labels": {
290+
Type: "object",
291+
AdditionalProperties: &extv1.JSONSchemaPropsOrBool{
292+
Allows: true,
293+
Schema: &extv1.JSONSchemaProps{Type: "string"},
294+
},
295+
},
296+
"annotations": {
297+
Type: "object",
298+
AdditionalProperties: &extv1.JSONSchemaPropsOrBool{
299+
Allows: true,
300+
Schema: &extv1.JSONSchemaProps{Type: "string"},
301+
},
302+
},
303+
},
304+
},
286305
},
287306
},
288307
}

0 commit comments

Comments
 (0)