Skip to content

Commit 6febbca

Browse files
aclevernamesenglezoushano
authored
chore: Duplicate upgrade-related information into resource bindings from RR (#774)
Co-authored-by: Stella Englezou <9325243+senglezou@users.noreply.github.com> Co-authored-by: Shane Dowling <shane@shanedowling.com>
1 parent 5723c61 commit 6febbca

11 files changed

Lines changed: 388 additions & 37 deletions

api/v1alpha1/resourcebinding_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ type ResourceBindingStatus struct {
6464
// +listMapKey=type
6565
// +optional
6666
Conditions []metav1.Condition `json:"conditions,omitempty"`
67+
// Reference to the Resource Request version
68+
// +optional
69+
LastAppliedVersion string `json:"lastAppliedVersion,omitempty"`
6770
}
6871

6972
// +kubebuilder:object:root=true

config/crd/bases/platform.kratix.io_resourcebindings.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ spec:
151151
x-kubernetes-list-map-keys:
152152
- type
153153
x-kubernetes-list-type: map
154+
lastAppliedVersion:
155+
description: Reference to the Resource Request version
156+
type: string
154157
type: object
155158
required:
156159
- spec

internal/controller/dynamic_resource_request_controller.go

Lines changed: 90 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"errors"
2222
"fmt"
23+
"maps"
2324
"strings"
2425

2526
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@@ -61,6 +62,8 @@ const (
6162
resourcePromiseVersionStatus = "promiseVersion"
6263
resourceBindingVersionStatus = "resourceBindingVersion"
6364
promiseRevisionLookupFailedReason = "FailedPromiseRevisionLookup"
65+
UnversionedPromiseVersion = "not-set"
66+
LatestVersion = "latest"
6467
)
6568

6669
type DynamicResourceRequestController struct {
@@ -79,7 +82,7 @@ type DynamicResourceRequestController struct {
7982
NumberOfJobsToKeep int
8083
ReconciliationInterval time.Duration
8184
EventRecorder record.EventRecorder
82-
PromiseUpgrade bool
85+
PromiseUpgradeFeatFlag bool
8386
}
8487

8588
//+kubebuilder:rbac:groups="batch",resources=jobs,verbs=get;list;watch;create;update;patch;delete
@@ -169,7 +172,7 @@ func (r *DynamicResourceRequestController) Reconcile(ctx context.Context, req ct
169172
promiseRevisionUsed *v1alpha1.PromiseRevision
170173
bindingVersion string
171174
)
172-
if r.PromiseUpgrade {
175+
if r.PromiseUpgradeFeatFlag {
173176
logging.Trace(baseLogger,
174177
"PromiseUpgrade feature flag set to true; will reconcile with a PromiseRevision.")
175178

@@ -205,18 +208,17 @@ func (r *DynamicResourceRequestController) Reconcile(ctx context.Context, req ct
205208
if resourceutil.FinalizersAreMissing(
206209
rr, []string{workFinalizer, removeAllWorkflowJobsFinalizer, runDeleteWorkflowsFinalizer},
207210
) {
208-
if err := addFinalizers(opts, rr, r.getRRFinalizers()); err != nil {
209-
if apierrors.IsConflict(err) {
210-
return fastRequeue, nil
211-
}
212-
return ctrl.Result{}, err
211+
err := addFinalizers(opts, rr, r.getRRFinalizers())
212+
if apierrors.IsConflict(err) {
213+
return fastRequeue, nil
213214
}
214-
return ctrl.Result{}, nil
215+
return ctrl.Result{}, err
215216
}
216217

217-
if r.PromiseUpgrade {
218+
if r.PromiseUpgradeFeatFlag {
218219
err := r.updateResourceBinding(ctx, logger, rr, promise)
219220
if err != nil {
221+
logging.Error(logger, err, "failed to update resource binding for resource request")
220222
return ctrl.Result{}, err
221223
}
222224
}
@@ -290,19 +292,19 @@ func (r *DynamicResourceRequestController) Reconcile(ctx context.Context, req ct
290292
return r.nextReconciliation(logger), r.cleanupWorkflowCountersAndExecution(ctx, logger, rr)
291293
}
292294

293-
rrNamespace := ""
294-
if promise.WorkflowPipelineNamespaceSet() {
295-
rrNamespace = rr.GetNamespace()
296-
}
297-
workLabels := resourceutil.GetWorkLabels(r.PromiseIdentifier, rr.GetName(), rrNamespace, "", v1alpha1.WorkTypeResource)
298-
299-
statusUpdate, err := r.generateResourceStatus(ctx, logger, rr, int64(len(pipelineResources)), workLabels, bindingVersion, promiseRevisionUsed)
295+
statusUpdated, err := r.ensureResourceStatus(ctx, logger, rr, promise, pipelineResources, bindingVersion, promiseRevisionUsed)
300296
if err != nil {
301297
return ctrl.Result{}, err
302298
}
299+
if statusUpdated {
300+
return ctrl.Result{}, nil
301+
}
303302

304-
if statusUpdate {
305-
return ctrl.Result{}, r.Client.Status().Update(ctx, rr)
303+
if r.PromiseUpgradeFeatFlag {
304+
if err := r.updateResourceBindingVersionStatus(ctx, logger, promise.GetName(), rr); err != nil {
305+
logging.Error(logger, err, "failed to update resource binding version status")
306+
return ctrl.Result{}, err
307+
}
306308
}
307309

308310
workflowCompletedCondition := resourceutil.GetCondition(rr, resourceutil.ConfigureWorkflowCompletedCondition)
@@ -316,7 +318,7 @@ func (r *DynamicResourceRequestController) Reconcile(ctx context.Context, req ct
316318
return r.nextReconciliation(logger), nil
317319
}
318320

319-
if r.PromiseUpgrade {
321+
if r.PromiseUpgradeFeatFlag {
320322
if r.updatePromiseVersionStatus(logger, rr, bindingVersion, promiseRevisionUsed) {
321323
return ctrl.Result{}, r.Client.Status().Update(ctx, rr)
322324
}
@@ -325,6 +327,53 @@ func (r *DynamicResourceRequestController) Reconcile(ctx context.Context, req ct
325327
return ctrl.Result{}, nil
326328
}
327329

330+
func (r *DynamicResourceRequestController) ensureResourceStatus(
331+
ctx context.Context,
332+
logger logr.Logger,
333+
rr *unstructured.Unstructured,
334+
promise *v1alpha1.Promise,
335+
pipelineResources []v1alpha1.PipelineJobResources,
336+
bindingVersion string,
337+
promiseRevisionUsed *v1alpha1.PromiseRevision,
338+
) (bool, error) {
339+
rrNamespace := ""
340+
if promise.WorkflowPipelineNamespaceSet() {
341+
rrNamespace = rr.GetNamespace()
342+
}
343+
workLabels := resourceutil.GetWorkLabels(r.PromiseIdentifier, rr.GetName(), rrNamespace, "", v1alpha1.WorkTypeResource)
344+
345+
statusUpdate, err := r.generateResourceStatus(ctx, logger, rr, int64(len(pipelineResources)), workLabels, bindingVersion, promiseRevisionUsed)
346+
if err != nil {
347+
return false, err
348+
}
349+
350+
if statusUpdate {
351+
return true, r.Client.Status().Update(ctx, rr)
352+
}
353+
return false, nil
354+
}
355+
356+
func (r *DynamicResourceRequestController) updateResourceBindingVersionStatus(ctx context.Context, logger logr.Logger, promiseName string, rr *unstructured.Unstructured) error {
357+
resourceBindingName := objectutil.GenerateDeterministicObjectName(fmt.Sprintf("%s-%s", rr.GetName(), promiseName))
358+
resourceBinding := &v1alpha1.ResourceBinding{}
359+
err := r.Client.Get(ctx, types.NamespacedName{Name: resourceBindingName, Namespace: rr.GetNamespace()}, resourceBinding)
360+
if err != nil {
361+
return fmt.Errorf("failed to get resourceBinding: %w", err)
362+
}
363+
364+
desiredVersion := resourceutil.GetStatus(rr, resourcePromiseVersionStatus)
365+
if resourceBinding.Status.LastAppliedVersion == desiredVersion {
366+
return nil
367+
}
368+
369+
logging.Info(logger, "updating resource binding Status.LastAppliedVersion", "oldVersion", resourceBinding.Status.LastAppliedVersion, "newVersion", desiredVersion)
370+
resourceBinding.Status.LastAppliedVersion = desiredVersion
371+
if err := r.Client.Status().Update(ctx, resourceBinding); err != nil {
372+
return fmt.Errorf("failed to update resource binding status: %w", err)
373+
}
374+
return nil
375+
}
376+
328377
func (r *DynamicResourceRequestController) updateResourceBinding(ctx context.Context, logger logr.Logger, rr *unstructured.Unstructured, promise *v1alpha1.Promise) error {
329378
resourceBinding := &v1alpha1.ResourceBinding{
330379
ObjectMeta: metav1.ObjectMeta{
@@ -333,9 +382,20 @@ func (r *DynamicResourceRequestController) updateResourceBinding(ctx context.Con
333382
},
334383
}
335384
op, err := controllerutil.CreateOrUpdate(ctx, r.Client, resourceBinding, func() error {
336-
resourceBinding.SetLabels(resourceBindingLabels(rr, promise))
385+
// Exclude one-shot trigger labels — propagating them would cause the
386+
// ResourceBinding controller to re-trigger reconciliation in an infinite loop.
387+
mergedLabels := rr.GetLabels()
388+
delete(mergedLabels, resourceutil.ManualReconciliationLabel)
389+
delete(mergedLabels, resourceutil.WorkflowRunFromStartLabel)
390+
if mergedLabels == nil {
391+
mergedLabels = make(map[string]string)
392+
}
393+
maps.Copy(mergedLabels, resourceBindingLabels(rr, promise))
394+
resourceBinding.SetLabels(mergedLabels)
337395
if resourceBinding.Spec.Version == "" {
338-
resourceBinding.Spec.Version = "latest"
396+
resourceBinding.Spec.Version = LatestVersion
397+
// if the resource binding got deleted, when we recreate the resource binding we infer what the resource binding
398+
// version used to be from the resource request `status.resourceBindingVersion`
339399
existingPromiseVersion := resourceutil.GetStatus(rr, resourceBindingVersionStatus)
340400
if existingPromiseVersion != "" {
341401
resourceBinding.Spec.Version = existingPromiseVersion
@@ -355,6 +415,7 @@ func (r *DynamicResourceRequestController) updateResourceBinding(ctx context.Con
355415
}
356416

357417
logging.Debug(logger, "ResourceBinding reconciled for Resource",
418+
"resourceBindingName", resourceBinding.GetName(),
358419
"operation", op,
359420
"promiseName", resourceBinding.Spec.PromiseRef.Name,
360421
"promiseVersion", resourceBinding.Spec.Version,
@@ -559,7 +620,7 @@ func (r *DynamicResourceRequestController) updateReconciledCondition(rr *unstruc
559620

560621
func (r *DynamicResourceRequestController) updatePromiseVersionStatus(logger logr.Logger, rr *unstructured.Unstructured, bindingVersion string, promiseRevision *v1alpha1.PromiseRevision) bool {
561622
logging.Trace(logger, "Checking if we need to update the promise version in the status")
562-
if !r.PromiseUpgrade || promiseRevision == nil {
623+
if !r.PromiseUpgradeFeatFlag || promiseRevision == nil {
563624
logging.Trace(logger, "Feature flag disabled or no PromiseRevision: no update promise version required")
564625
return false
565626
}
@@ -761,7 +822,7 @@ func (r *DynamicResourceRequestController) deleteResources(o opts, promise *v1al
761822
return fastRequeue, nil
762823
}
763824

764-
if r.PromiseUpgrade {
825+
if r.PromiseUpgradeFeatFlag {
765826
if controllerutil.ContainsFinalizer(resourceRequest, resourceBindingFinalizer) {
766827
err := r.deleteResourceBinding(o, resourceRequest, promise, resourceBindingFinalizer)
767828
if err != nil {
@@ -1041,6 +1102,11 @@ func latestRevision(ctx context.Context, c client.Client, promise *v1alpha1.Prom
10411102
func fetchRevision(ctx context.Context, c client.Client, promise *v1alpha1.Promise,
10421103
binding *v1alpha1.ResourceBinding, promiseVersionFromRRStatus string) (*v1alpha1.PromiseRevision, error) {
10431104

1105+
// there is a scenario where the PromiseVersion from resource request status
1106+
// is set, but no resource binding exists, which means the resource binding was
1107+
// deleted at some point.
1108+
//
1109+
// We infer what the resource binding version used to be from the resource request `status.resourceBindingVersion`
10441110
desiredVersion := promiseVersionFromRRStatus
10451111

10461112
if binding != nil {
@@ -1098,7 +1164,7 @@ func (r *DynamicResourceRequestController) setPromiseLabels(ctx context.Context,
10981164

10991165
func (r *DynamicResourceRequestController) getRRFinalizers() []string {
11001166
rrFinalizers := []string{workFinalizer, removeAllWorkflowJobsFinalizer, runDeleteWorkflowsFinalizer}
1101-
if r.PromiseUpgrade {
1167+
if r.PromiseUpgradeFeatFlag {
11021168
rrFinalizers = append(rrFinalizers, resourceBindingFinalizer)
11031169
}
11041170
return rrFinalizers

internal/controller/dynamic_resource_request_controller_test.go

Lines changed: 120 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,7 +1203,7 @@ var _ = Describe("DynamicResourceRequestController", func() {
12031203
When("promise upgrade feature is on", func() {
12041204
BeforeEach(func() {
12051205
Expect(fakeK8sClient.Delete(ctx, resReq)).To(Succeed())
1206-
reconciler.PromiseUpgrade = true
1206+
reconciler.PromiseUpgradeFeatFlag = true
12071207
resReq = createResourceRequest(resourceRequestPath)
12081208
resReqNameNamespace = client.ObjectKeyFromObject(resReq)
12091209
})
@@ -1476,6 +1476,124 @@ var _ = Describe("DynamicResourceRequestController", func() {
14761476
})
14771477
})
14781478

1479+
When("updating the resource binding version status", func() {
1480+
const promiseVersion = "v1.1.0"
1481+
1482+
BeforeEach(func() {
1483+
createPromiseRevision(fakeK8sClient, promise, promiseVersion)
1484+
})
1485+
1486+
When("the resource binding Status.LastAppliedVersion does not yet reflect the current promise version", func() {
1487+
It("updates Status.LastAppliedVersion to track the current promise version", func() {
1488+
setReconcileConfigureWorkflowToReturnFinished()
1489+
result, err := t.reconcileUntilCompletion(reconciler, resReq)
1490+
Expect(err).NotTo(HaveOccurred())
1491+
Expect(result).To(Equal(ctrl.Result{}))
1492+
1493+
binding := getResourceBinding(promise.GetName(), resReqNameNamespace)
1494+
Expect(binding.Spec.Version).To(Equal("latest"))
1495+
Expect(binding.Status.LastAppliedVersion).To(Equal(promiseVersion))
1496+
})
1497+
})
1498+
1499+
When("the resource binding is pinned to a specific promise version", func() {
1500+
BeforeEach(func() {
1501+
createResourceBinding(fakeK8sClient, promise, resReq, promiseVersion)
1502+
})
1503+
1504+
It("updates Status.LastAppliedVersion to reflect the current promise version", func() {
1505+
setReconcileConfigureWorkflowToReturnFinished()
1506+
result, err := t.reconcileUntilCompletion(reconciler, resReq)
1507+
Expect(err).NotTo(HaveOccurred())
1508+
Expect(result).To(Equal(ctrl.Result{}))
1509+
1510+
binding := getResourceBinding(promise.GetName(), resReqNameNamespace)
1511+
Expect(binding.Spec.Version).To(Equal(promiseVersion))
1512+
Expect(binding.Status.LastAppliedVersion).To(Equal(promiseVersion))
1513+
})
1514+
})
1515+
})
1516+
1517+
When("creating the resource binding", func() {
1518+
BeforeEach(func() {
1519+
createPromiseRevision(fakeK8sClient, promise, "v1.1.0")
1520+
})
1521+
1522+
It("merges the resource request's labels with the binding-specific labels", func() {
1523+
setReconcileConfigureWorkflowToReturnFinished()
1524+
result, err := t.reconcileUntilCompletion(reconciler, resReq)
1525+
Expect(err).NotTo(HaveOccurred())
1526+
Expect(result).To(Equal(ctrl.Result{}))
1527+
1528+
binding := getResourceBinding(promise.GetName(), resReqNameNamespace)
1529+
// Labels originating from the resource request itself
1530+
Expect(binding.GetLabels()).To(HaveKeyWithValue("non-kratix-label", "true"))
1531+
// Binding-specific labels added by resourceBindingLabels
1532+
Expect(binding.GetLabels()).To(HaveKeyWithValue("kratix.io/promise-name", promise.GetName()))
1533+
Expect(binding.GetLabels()).To(HaveKeyWithValue("kratix.io/resource-name", resReq.GetName()))
1534+
})
1535+
1536+
It("does not propagate ephemeral trigger labels from the resource request to the binding", func() {
1537+
Expect(fakeK8sClient.Get(ctx, resReqNameNamespace, resReq)).To(Succeed())
1538+
labels := resReq.GetLabels()
1539+
labels[resourceutil.ManualReconciliationLabel] = "true"
1540+
labels[resourceutil.WorkflowRunFromStartLabel] = "true"
1541+
resReq.SetLabels(labels)
1542+
Expect(fakeK8sClient.Update(ctx, resReq)).To(Succeed())
1543+
1544+
setReconcileConfigureWorkflowToReturnFinished()
1545+
result, err := t.reconcileUntilCompletion(reconciler, resReq)
1546+
Expect(err).NotTo(HaveOccurred())
1547+
Expect(result).To(Equal(ctrl.Result{}))
1548+
1549+
binding := getResourceBinding(promise.GetName(), resReqNameNamespace)
1550+
Expect(binding.GetLabels()).NotTo(HaveKey(resourceutil.ManualReconciliationLabel))
1551+
Expect(binding.GetLabels()).NotTo(HaveKey(resourceutil.WorkflowRunFromStartLabel))
1552+
})
1553+
})
1554+
1555+
When("updating the labels on an existing resource binding", func() {
1556+
BeforeEach(func() {
1557+
createPromiseRevision(fakeK8sClient, promise, "v1.1.0")
1558+
setReconcileConfigureWorkflowToReturnFinished()
1559+
_, err := t.reconcileUntilCompletion(reconciler, resReq)
1560+
Expect(err).NotTo(HaveOccurred())
1561+
})
1562+
1563+
It("reflects a changed label from the resource request on the binding", func() {
1564+
Expect(fakeK8sClient.Get(ctx, resReqNameNamespace, resReq)).To(Succeed())
1565+
updatedLabels := resReq.GetLabels()
1566+
updatedLabels["non-kratix-label"] = "updated-value"
1567+
resReq.SetLabels(updatedLabels)
1568+
Expect(fakeK8sClient.Update(ctx, resReq)).To(Succeed())
1569+
1570+
result, err := t.reconcileUntilCompletion(reconciler, resReq)
1571+
Expect(err).NotTo(HaveOccurred())
1572+
Expect(result).To(Equal(ctrl.Result{}))
1573+
1574+
binding := getResourceBinding(promise.GetName(), resReqNameNamespace)
1575+
Expect(binding.GetLabels()).To(HaveKeyWithValue("non-kratix-label", "updated-value"))
1576+
})
1577+
1578+
It("removes a label from the binding when it is removed from the resource request", func() {
1579+
Expect(fakeK8sClient.Get(ctx, resReqNameNamespace, resReq)).To(Succeed())
1580+
updatedLabels := resReq.GetLabels()
1581+
delete(updatedLabels, "non-kratix-label")
1582+
resReq.SetLabels(updatedLabels)
1583+
Expect(fakeK8sClient.Update(ctx, resReq)).To(Succeed())
1584+
1585+
result, err := t.reconcileUntilCompletion(reconciler, resReq)
1586+
Expect(err).NotTo(HaveOccurred())
1587+
Expect(result).To(Equal(ctrl.Result{}))
1588+
1589+
binding := getResourceBinding(promise.GetName(), resReqNameNamespace)
1590+
Expect(binding.GetLabels()).NotTo(HaveKey("non-kratix-label"))
1591+
// Binding-specific labels should remain untouched
1592+
Expect(binding.GetLabels()).To(HaveKeyWithValue("kratix.io/promise-name", promise.GetName()))
1593+
Expect(binding.GetLabels()).To(HaveKeyWithValue("kratix.io/resource-name", resReq.GetName()))
1594+
})
1595+
})
1596+
14791597
})
14801598
})
14811599

@@ -1523,7 +1641,7 @@ func createPromiseRevision(fakeK8sClient client.Client, promise *v1alpha1.Promis
15231641
Expect(fakeK8sClient.Status().Update(ctx, promiseRevision)).To(Succeed())
15241642
}
15251643

1526-
func createResourceBinding(client client.Client, promise *v1alpha1.Promise, rr *unstructured.Unstructured, version string) *v1alpha1.ResourceBinding {
1644+
func createResourceBinding(client client.Client, promise *v1alpha1.Promise, rr *unstructured.Unstructured, version string) {
15271645
resourceBinding := &v1alpha1.ResourceBinding{
15281646
ObjectMeta: metav1.ObjectMeta{
15291647
Name: objectutil.GenerateDeterministicObjectName(fmt.Sprintf("%s-%s", rr.GetName(), promise.GetName())),
@@ -1545,7 +1663,6 @@ func createResourceBinding(client client.Client, promise *v1alpha1.Promise, rr *
15451663
},
15461664
}
15471665
ExpectWithOffset(1, client.Create(ctx, resourceBinding)).To(Succeed())
1548-
return resourceBinding
15491666
}
15501667

15511668
func setConfigureWorkflowStatus(resReq *unstructured.Unstructured, status v1.ConditionStatus, lastTransitionTime ...time.Time) {

0 commit comments

Comments
 (0)