Skip to content

Commit 81c4c91

Browse files
authored
Merge pull request #812 from bschaatsbergen/fix/dont-apply-externalrefs
fix: exclude externalRef nodes from applySet to prevent empty CRs
2 parents 849665d + 5c580be commit 81c4c91

File tree

14 files changed

+312
-43
lines changed

14 files changed

+312
-43
lines changed

pkg/applyset/applyset.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -554,24 +554,31 @@ func (a *applySet) apply(ctx context.Context, dryRun bool) (*ApplyResult, error)
554554
if err != nil {
555555
return results, err
556556
}
557+
558+
// Apply resources using server-side apply
557559
eg.Go(func() error {
558-
lastApplied, err := dynResource.Apply(egctx, obj.GetName(), obj.Unstructured, options)
560+
lastApplied, err := a.applyResource(egctx, dynResource, obj, options)
559561
mu.Lock()
560562
defer mu.Unlock()
561563
results.recordApplied(obj, lastApplied, err)
562-
var appliedRevision string
563-
if lastApplied != nil {
564-
appliedRevision = lastApplied.GetResourceVersion()
565-
}
566-
a.log.V(2).Info("applied object", "object", obj.String(), "applied-revision", appliedRevision,
567-
"error", err)
568564
return nil
569565
})
570566
}
571567

572568
return results, eg.Wait()
573569
}
574570

571+
// applyResource applies a resource to the cluster using server-side apply.
572+
func (a *applySet) applyResource(ctx context.Context, dynResource dynamic.ResourceInterface, obj ApplyableObject, options metav1.ApplyOptions) (*unstructured.Unstructured, error) {
573+
resource, err := dynResource.Apply(ctx, obj.GetName(), obj.Unstructured, options)
574+
var appliedRevision string
575+
if resource != nil {
576+
appliedRevision = resource.GetResourceVersion()
577+
}
578+
a.log.V(2).Info("applied resource", "object", obj.String(), "applied-revision", appliedRevision, "error", err)
579+
return resource, err
580+
}
581+
575582
func (a *applySet) prune(ctx context.Context, results *ApplyResult, dryRun bool) (*ApplyResult, error) {
576583
pruneObjects, err := a.findAllObjectsToPrune(ctx, a.dynamicClient, results.AppliedUIDs())
577584
if err != nil {

pkg/applyset/tracker.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,6 @@ type ApplyableObject struct {
5050
// It is opaque and is passed in the callbacks as is
5151
ID string
5252

53-
// Lifecycle hints
54-
// TODO(barney-s): need to exapnd on these: https://github.com/kubernetes-sigs/kro/issues/542
55-
ExternalRef bool
56-
5753
// lastReadRevision is the revision of the object that was last read from the cluster.
5854
lastReadRevision string
5955
}

pkg/controller/instance/controller_reconcile.go

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ type instanceGraphReconciler struct {
9898
// It manages the full lifecycle of the instance including creation, updates,
9999
// and deletion.
100100
func (igr *instanceGraphReconciler) reconcile(ctx context.Context) error {
101+
igr.log.V(2).Info("reconciling instance")
102+
101103
igr.state = newInstanceState()
102104

103105
// Create runtime - if this fails, the defer in Controller.Reconcile handles status
@@ -225,10 +227,28 @@ func (igr *instanceGraphReconciler) reconcileInstance(ctx context.Context) error
225227
break
226228
}
227229

230+
// ExternalRefs are read-only - fetch them directly instead of adding to applyset
231+
if igr.runtime.ResourceDescriptor(resourceID).IsExternalRef() {
232+
clusterObj, err := igr.readExternalRef(ctx, resourceID, resource)
233+
if err != nil {
234+
resourceState.State = ResourceStateError
235+
resourceState.Err = fmt.Errorf("failed to read external ref: %w", err)
236+
break
237+
}
238+
igr.runtime.SetResource(resourceID, clusterObj)
239+
igr.updateResourceReadiness(resourceID)
240+
// Synchronize runtime state after each resource to re-evaluate CEL expressions
241+
if _, err := igr.runtime.Synchronize(); err != nil {
242+
return fmt.Errorf("failed to synchronize after reading external ref: %w", err)
243+
}
244+
resourceState.State = ResourceStateSynced
245+
continue
246+
}
247+
248+
// Regular resources go through the applyset
228249
applyable := applyset.ApplyableObject{
229250
Unstructured: resource,
230251
ID: resourceID,
231-
ExternalRef: igr.runtime.ResourceDescriptor(resourceID).IsExternalRef(),
232252
}
233253
clusterObj, err := aset.Add(ctx, applyable)
234254
if err != nil {
@@ -273,7 +293,7 @@ func (igr *instanceGraphReconciler) reconcileInstance(ctx context.Context) error
273293

274294
if err := result.Errors(); err != nil {
275295
mark.ResourcesNotReady("there was an error while reconciling resources in the apply set: %v", err)
276-
return fmt.Errorf("failed to apply/prune resources: %w", err)
296+
return igr.delayedRequeue(fmt.Errorf("failed to apply/prune resources: %w", err))
277297
}
278298

279299
if unresolvedResourceID != "" {
@@ -548,6 +568,32 @@ func (igr *instanceGraphReconciler) delayedRequeue(err error) error {
548568
return requeue.NeededAfter(err, igr.reconcileConfig.DefaultRequeueDuration)
549569
}
550570

571+
// readExternalRef fetches an external reference from the cluster.
572+
// External references are resources that exist outside of this instance's control.
573+
func (igr *instanceGraphReconciler) readExternalRef(ctx context.Context, resourceID string, resource *unstructured.Unstructured) (*unstructured.Unstructured, error) {
574+
gvk := resource.GroupVersionKind()
575+
restMapping, err := igr.restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
576+
if err != nil {
577+
return nil, fmt.Errorf("failed to get REST mapping for %v: %w", gvk, err)
578+
}
579+
580+
var dynResource dynamic.ResourceInterface
581+
if restMapping.Scope.Name() == meta.RESTScopeNameNamespace {
582+
namespace := igr.getResourceNamespace(resourceID)
583+
dynResource = igr.client.Resource(restMapping.Resource).Namespace(namespace)
584+
} else {
585+
dynResource = igr.client.Resource(restMapping.Resource)
586+
}
587+
588+
clusterObj, err := dynResource.Get(ctx, resource.GetName(), metav1.GetOptions{})
589+
if err != nil {
590+
return nil, fmt.Errorf("failed to get external ref %s/%s: %w", resource.GetNamespace(), resource.GetName(), err)
591+
}
592+
593+
igr.log.V(2).Info("read external ref", "gvk", gvk, "namespace", resource.GetNamespace(), "name", resource.GetName())
594+
return clusterObj, nil
595+
}
596+
551597
// getResourceNamespace determines the appropriate namespace for a resource.
552598
// It follows this precedence order:
553599
// 1. Resource's explicitly specified namespace

pkg/dynamiccontroller/dynamic_controller.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ func (dc *DynamicController) processNextWorkItem(ctx context.Context) bool {
270270
}
271271

272272
err := dc.syncFunc(ctx, item, handler.(Handler))
273-
if err == nil || apierrors.IsNotFound(err) {
273+
if err == nil {
274274
dc.queue.Forget(item)
275275
return true
276276
}
@@ -291,6 +291,12 @@ func (dc *DynamicController) processNextWorkItem(ctx context.Context) bool {
291291
requeueTotal.WithLabelValues(gvrKey, "requeue_after").Inc()
292292
dc.queue.AddAfter(item, typedErr.Duration())
293293
default:
294+
// we only check here for this not found error here because we want explicit requeue signals to have priority
295+
if apierrors.IsNotFound(err) {
296+
dc.log.V(1).Info("item no longer exists, dropping from queue", "item", item)
297+
dc.queue.Forget(item)
298+
return true
299+
}
294300
requeueTotal.WithLabelValues(gvrKey, "rate_limited").Inc()
295301
if dc.queue.NumRequeues(item) < dc.config.QueueMaxRetries {
296302
dc.log.Error(err, "Error syncing item, requeuing with rate limit", "item", item)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
apiVersion: chainsaw.kyverno.io/v1alpha1
2+
kind: Test
3+
metadata:
4+
name: check-external-references
5+
spec:
6+
description: |
7+
Tests if a ResourceGraphDefinition creates an Instance CRD
8+
steps:
9+
- name: setup
10+
try:
11+
- apply:
12+
file: rgd.yaml
13+
description: Apply RGD
14+
- assert:
15+
file: rgd-assert.yaml
16+
description: Verify RGD state
17+
- apply:
18+
file: instance.yaml
19+
description: Apply ExternalReferenceTest Instance
20+
finally:
21+
- description: sleep operation to let instance settle
22+
sleep:
23+
duration: 3s
24+
catch:
25+
- description: kro controller pod logs collector
26+
podLogs:
27+
selector: app.kubernetes.io/instance=kro
28+
namespace: kro-system
29+
30+
- name: check-instance-state
31+
try:
32+
- assert:
33+
file: instance-assert-in-progress.yaml
34+
description: Verify Instance State is in progress (waiting for external resource)
35+
- error:
36+
file: configmap.yaml
37+
description: Verify ConfigMap does not exist
38+
- error:
39+
file: owned.yaml
40+
description: Verify Owned Resource does not exist
41+
catch:
42+
- description: kro controller pod logs collector
43+
podLogs:
44+
selector: app.kubernetes.io/instance=kro
45+
namespace: kro-system
46+
47+
- name: ensure-external-ref
48+
try:
49+
- apply:
50+
file: configmap.yaml
51+
description: Apply External Reference ConfigMap
52+
- assert:
53+
file: instance-assert.yaml
54+
description: Verify Instance becomes Ready after Reference is fulfilled
55+
- assert:
56+
file: owned.yaml
57+
description: Verify Owned Resource based on External Reference is created
58+
catch:
59+
- description: kro controller pod logs collector
60+
podLogs:
61+
selector: app.kubernetes.io/instance=kro
62+
namespace: kro-system
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: v1
2+
kind: ConfigMap
3+
metadata:
4+
name: check-external-references
5+
namespace: default
6+
data:
7+
ECHO_VALUE: "Hello, World!"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
apiVersion: kro.run/v1alpha1
2+
kind: ExternalReferenceTest
3+
metadata:
4+
finalizers:
5+
- kro.run/finalizer
6+
generation: 1
7+
labels:
8+
kro.run/owned: "true"
9+
kro.run/resource-graph-definition-name: check-external-references
10+
name: test-external-reference
11+
spec:
12+
name: foobar
13+
status:
14+
state: IN_PROGRESS
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
apiVersion: kro.run/v1alpha1
2+
kind: ExternalReferenceTest
3+
metadata:
4+
finalizers:
5+
- kro.run/finalizer
6+
labels:
7+
kro.run/owned: "true"
8+
kro.run/resource-graph-definition-name: check-external-references
9+
name: test-external-reference
10+
spec:
11+
name: foobar
12+
status:
13+
# filter conditions array to keep elements where `type == 'Ready'`
14+
# and assert there's a single element matching the filter
15+
# and that this element status is `True`
16+
(conditions[?type == 'Ready']):
17+
- status: 'True'
18+
state: ACTIVE
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: ExternalReferenceTest
2+
apiVersion: kro.run/v1alpha1
3+
metadata:
4+
name: test-external-reference
5+
spec:
6+
name: propagate
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
kind: ExternalReferenceTest
2+
apiVersion: kro.run/v1alpha1
3+
metadata:
4+
name: test-external-reference

0 commit comments

Comments
 (0)