Skip to content

Commit 36a1378

Browse files
authored
Refactor handling of references (#240)
1 parent d603faf commit 36a1378

File tree

7 files changed

+70
-167
lines changed

7 files changed

+70
-167
lines changed

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ require (
8282
github.com/x448/float16 v0.8.4 // indirect
8383
github.com/xlab/treeprint v1.2.0 // indirect
8484
golang.org/x/crypto v0.31.0 // indirect
85-
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
8685
golang.org/x/net v0.33.0 // indirect
8786
golang.org/x/oauth2 v0.24.0 // indirect
8887
golang.org/x/sync v0.10.0 // indirect

go.sum

Lines changed: 2 additions & 109 deletions
Large diffs are not rendered by default.

pkg/component/component.go

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@ SPDX-License-Identifier: Apache-2.0
66
package component
77

88
import (
9-
"encoding/json"
109
"fmt"
1110
"reflect"
1211
"time"
1312

14-
"github.com/sap/component-operator-runtime/internal/walk"
1513
"github.com/sap/component-operator-runtime/pkg/reconciler"
1614
)
1715

@@ -120,41 +118,6 @@ func assertTypeConfiguration[T Component](component T) (TypeConfiguration, bool)
120118
return nil, false
121119
}
122120

123-
// Calculate digest of given component, honoring annotations, spec, and references.
124-
func calculateComponentDigest[T Component](component T) string {
125-
digestData := make(map[string]any)
126-
spec := getSpec(component)
127-
digestData["annotations"] = component.GetAnnotations()
128-
digestData["spec"] = spec
129-
if err := walk.Walk(getSpec(component), func(x any, path []string, _ reflect.StructTag) error {
130-
// note: this must() is ok because marshalling []string should always work
131-
rawPath := must(json.Marshal(path))
132-
switch r := x.(type) {
133-
case *ConfigMapReference:
134-
if r != nil {
135-
digestData["refs:"+string(rawPath)] = r.digest()
136-
}
137-
case *ConfigMapKeyReference:
138-
if r != nil {
139-
digestData["refs:"+string(rawPath)] = r.digest()
140-
}
141-
case *SecretReference:
142-
if r != nil {
143-
digestData["refs:"+string(rawPath)] = r.digest()
144-
}
145-
case *SecretKeyReference:
146-
if r != nil {
147-
digestData["refs:"+string(rawPath)] = r.digest()
148-
}
149-
}
150-
return nil
151-
}); err != nil {
152-
// note: this panic is ok because walk.Walk() only produces errors if the given walker function raises any (which ours here does not do)
153-
panic("this cannot happen")
154-
}
155-
return calculateDigest(digestData)
156-
}
157-
158121
// Implement the PlacementConfiguration interface.
159122
func (s *PlacementSpec) GetDeploymentNamespace() string {
160123
return s.Namespace

pkg/component/reconciler.go

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ const (
8484
// manager), and the current (potentially unsaved) state of the component.
8585
// Post-hooks will only be called if the according operation (read, reconcile, delete)
8686
// has been successful.
87+
// Note: hooks may change the status of the component, but must not alter the metadata or spec,
88+
// since changes might be persisted by the framework (e.g. when updating finalizers).
8789
type HookFunc[T Component] func(ctx context.Context, clnt client.Client, component T) error
8890

8991
// NewClientFunc is the function signature that can be used to modify or replace the default
@@ -181,9 +183,8 @@ func NewReconciler[T Component](name string, resourceGenerator manifests.Generat
181183
statusAnalyzer: status.NewStatusAnalyzer(name),
182184
options: options,
183185
// TODO: make backoff configurable via options?
184-
backoff: backoff.NewBackoff(10 * time.Second),
185-
postReadHooks: []HookFunc[T]{resolveReferences[T]},
186-
triggerCh: make(chan event.TypedGenericEvent[apitypes.NamespacedName], triggerBufferSize),
186+
backoff: backoff.NewBackoff(10 * time.Second),
187+
triggerCh: make(chan event.TypedGenericEvent[apitypes.NamespacedName], triggerBufferSize),
187188
}
188189
}
189190

@@ -203,7 +204,7 @@ func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (result
203204

204205
now := metav1.Now()
205206

206-
// fetch reconciled object
207+
// fetch reconciled component
207208
component := newComponent[T]()
208209
if err := r.client.Get(ctx, req.NamespacedName, component); err != nil {
209210
if apierrors.IsNotFound(err) {
@@ -213,6 +214,8 @@ func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (result
213214
return ctrl.Result{}, errors.Wrap(err, "unexpected get error")
214215
}
215216
component.GetObjectKind().SetGroupVersionKind(r.groupVersionKind)
217+
// componentDigest is populated after post-read hook phase
218+
componentDigest := ""
216219

217220
// fetch requeue interval, retry interval and timeout
218221
requeueInterval := time.Duration(0)
@@ -306,14 +309,24 @@ func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (result
306309
}
307310
}
308311

309-
// TODO: should we move this behind the DeepEqual check below to avoid noise?
312+
// TODO: should we move this behind the DeepEqual check below to reduce noise?
310313
// also note: it seems that no events will be written if the component's namespace is in deletion
311314
state, reason, message := status.GetState()
312315
var eventAnnotations map[string]string
313316
// TODO: formalize this into a real published interface
314-
if eventAnnotationProvider, ok := Component(component).(interface{ GetEventAnnotations() map[string]string }); ok {
315-
eventAnnotations = eventAnnotationProvider.GetEventAnnotations()
316-
}
317+
// TODO: pass previousState, and especially componentDigest in a better way;
318+
// maybe we could even make the component aware of its own digest ...
319+
// another option could be to model this as a hook-like function (instead of a component method) ...
320+
// note: the passed component digest might be empty (that is, if we return before the post-read phase)
321+
// note: this interface is not released for usage; it may change without announcement
322+
if eventAnnotationProvider, ok := Component(component).(interface {
323+
GetEventAnnotations(previousState State, componentDigest string) map[string]string
324+
}); ok {
325+
eventAnnotations = eventAnnotationProvider.GetEventAnnotations(savedStatus.State, componentDigest)
326+
}
327+
// TODO: sending events may block a little while (some seconds), in particular if enhanced recorders are installed through options.NewClient(),
328+
// such as the flux notfication recorder; should we therefore send the events asynchronously, or start synchronously and continue asynchronous
329+
// after a little while?
317330
if state == StateError {
318331
r.client.EventRecorder().AnnotatedEventf(component, eventAnnotations, corev1.EventTypeWarning, reason, message)
319332
} else {
@@ -350,6 +363,12 @@ func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (result
350363
return ctrl.Result{Requeue: true}, nil
351364
}
352365

366+
// resolve references
367+
componentDigest, err = resolveReferences(ctx, r.client, component)
368+
if err != nil {
369+
return ctrl.Result{}, errors.Wrap(err, "error resolving references")
370+
}
371+
353372
// run post-read hooks
354373
// note: it's important that this happens after deferring the status handler
355374
// TODO: enhance ctx with tailored logger and event recorder
@@ -400,7 +419,7 @@ func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (result
400419
return ctrl.Result{}, errors.Wrapf(err, "error running pre-reconcile hook (%d)", hookOrder)
401420
}
402421
}
403-
ok, digest, err := target.Apply(ctx, component)
422+
ok, processingDigest, err := target.Apply(ctx, component, componentDigest)
404423
if err != nil {
405424
log.V(1).Info("error while reconciling dependent resources")
406425
return ctrl.Result{}, errors.Wrap(err, "error reconciling dependent resources")
@@ -418,8 +437,8 @@ func (r *Reconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (result
418437
return ctrl.Result{RequeueAfter: requeueInterval}, nil
419438
} else {
420439
log.V(1).Info("not all dependent resources successfully reconciled")
421-
if digest != status.ProcessingDigest {
422-
status.ProcessingDigest = digest
440+
if processingDigest != status.ProcessingDigest {
441+
status.ProcessingDigest = processingDigest
423442
status.ProcessingSince = &now
424443
r.backoff.Forget(req)
425444
}

pkg/component/reference.go

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package component
77

88
import (
99
"context"
10+
"encoding/json"
1011
"fmt"
1112
"reflect"
1213
"strings"
@@ -66,6 +67,7 @@ func (r *ConfigMapReference) load(ctx context.Context, clnt client.Client, names
6667

6768
func (r *ConfigMapReference) digest() string {
6869
if !r.loaded {
70+
// TODO: shouldn't we panic here?
6971
return ""
7072
}
7173
return calculateDigest(r.data)
@@ -128,6 +130,7 @@ func (r *ConfigMapKeyReference) load(ctx context.Context, clnt client.Client, na
128130

129131
func (r *ConfigMapKeyReference) digest() string {
130132
if !r.loaded {
133+
// TODO: shouldn't we panic here?
131134
return ""
132135
}
133136
return sha256hex([]byte(r.value))
@@ -172,6 +175,7 @@ func (r *SecretReference) load(ctx context.Context, clnt client.Client, namespac
172175

173176
func (r *SecretReference) digest() string {
174177
if !r.loaded {
178+
// TODO: shouldn't we panic here?
175179
return ""
176180
}
177181
return calculateDigest(r.data)
@@ -234,6 +238,7 @@ func (r *SecretKeyReference) load(ctx context.Context, clnt client.Client, names
234238

235239
func (r *SecretKeyReference) digest() string {
236240
if !r.loaded {
241+
// TODO: shouldn't we panic here?
237242
return ""
238243
}
239244
return sha256hex(r.value)
@@ -248,15 +253,26 @@ func (r *SecretKeyReference) Value() []byte {
248253
return r.value
249254
}
250255

251-
func resolveReferences[T Component](ctx context.Context, clnt client.Client, component T) error {
252-
return walk.Walk(getSpec(component), func(x any, path []string, tag reflect.StructTag) error {
256+
func resolveReferences[T Component](ctx context.Context, clnt client.Client, component T) (string, error) {
257+
digestData := make(map[string]any)
258+
spec := getSpec(component)
259+
digestData["generation"] = component.GetGeneration()
260+
digestData["annotations"] = component.GetAnnotations()
261+
digestData["spec"] = spec
262+
if err := walk.Walk(spec, func(x any, path []string, tag reflect.StructTag) error {
263+
// note: this must() is ok because marshalling []string should always work
264+
rawPath := must(json.Marshal(path))
265+
// TODO: allow arbitrary loadable types (with an interface LoadableReference or similar)
253266
switch r := x.(type) {
254267
case *ConfigMapReference:
255268
if r == nil {
256269
return nil
257270
}
258271
ignoreNotFound := !component.GetDeletionTimestamp().IsZero() && tag.Get(tagNotFoundPolicy) == notFoundPolicyIgnoreOnDeletion
259-
return r.load(ctx, clnt, component.GetNamespace(), ignoreNotFound)
272+
if err := r.load(ctx, clnt, component.GetNamespace(), ignoreNotFound); err != nil {
273+
return err
274+
}
275+
digestData["refs:"+string(rawPath)] = r.digest()
260276
case *ConfigMapKeyReference:
261277
if r == nil {
262278
return nil
@@ -266,13 +282,19 @@ func resolveReferences[T Component](ctx context.Context, clnt client.Client, com
266282
if s := tag.Get(tagFallbackKeys); s != "" {
267283
fallbackKeys = strings.Split(s, ",")
268284
}
269-
return r.load(ctx, clnt, component.GetNamespace(), ignoreNotFound, fallbackKeys...)
285+
if err := r.load(ctx, clnt, component.GetNamespace(), ignoreNotFound, fallbackKeys...); err != nil {
286+
return err
287+
}
288+
digestData["refs:"+string(rawPath)] = r.digest()
270289
case *SecretReference:
271290
if r == nil {
272291
return nil
273292
}
274293
ignoreNotFound := !component.GetDeletionTimestamp().IsZero() && tag.Get(tagNotFoundPolicy) == notFoundPolicyIgnoreOnDeletion
275-
return r.load(ctx, clnt, component.GetNamespace(), ignoreNotFound)
294+
if err := r.load(ctx, clnt, component.GetNamespace(), ignoreNotFound); err != nil {
295+
return err
296+
}
297+
digestData["refs:"+string(rawPath)] = r.digest()
276298
case *SecretKeyReference:
277299
if r == nil {
278300
return nil
@@ -282,8 +304,14 @@ func resolveReferences[T Component](ctx context.Context, clnt client.Client, com
282304
if s := tag.Get(tagFallbackKeys); s != "" {
283305
fallbackKeys = strings.Split(s, ",")
284306
}
285-
return r.load(ctx, clnt, component.GetNamespace(), ignoreNotFound, fallbackKeys...)
307+
if err := r.load(ctx, clnt, component.GetNamespace(), ignoreNotFound, fallbackKeys...); err != nil {
308+
return err
309+
}
310+
digestData["refs:"+string(rawPath)] = r.digest()
286311
}
287312
return nil
288-
})
313+
}); err != nil {
314+
return "", err
315+
}
316+
return calculateDigest(digestData), nil
289317
}

pkg/component/target.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func newReconcileTarget[T Component](reconcilerName string, reconcilerId string,
3535
}
3636
}
3737

38-
func (t *reconcileTarget[T]) Apply(ctx context.Context, component T) (bool, string, error) {
38+
func (t *reconcileTarget[T]) Apply(ctx context.Context, component T, componentDigest string) (bool, string, error) {
3939
//log := log.FromContext(ctx)
4040
namespace := ""
4141
name := ""
@@ -51,7 +51,6 @@ func (t *reconcileTarget[T]) Apply(ctx context.Context, component T) (bool, stri
5151
}
5252
ownerId := t.reconcilerId + "/" + component.GetNamespace() + "/" + component.GetName()
5353
status := component.GetStatus()
54-
componentDigest := calculateComponentDigest(component)
5554

5655
// TODO: enhance ctx with local client
5756
generateCtx := NewContext(ctx).

pkg/reconciler/reconciler.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,8 @@ func NewReconciler(name string, clnt cluster.Client, options ReconcilerOptions)
243243
// otherwise, if it returns false, the caller should re-call it periodically, until it returns true. In any case, the passed inventory should match the state of the
244244
// inventory after the previous invocation of Apply(); usually, the caller saves the inventory after calling Apply(), and loads it before calling Apply().
245245
// The namespace and ownerId arguments should not be changed across subsequent invocations of Apply(); the componentRevision should be incremented only.
246+
//
247+
// Also note: it is absolutely crucial that this method returns (true, nil) immediately (on the first call) if everything is already in the right state.
246248
func (r *Reconciler) Apply(ctx context.Context, inventory *[]*InventoryItem, objects []client.Object, namespace string, ownerId string, componentRevision int64) (bool, error) {
247249
var err error
248250
log := log.FromContext(ctx)

0 commit comments

Comments
 (0)