feat: restore nested cross-resource references after Create and Update - #267
Open
gustavodiaz7722 wants to merge 1 commit into
Open
feat: restore nested cross-resource references after Create and Update#267gustavodiaz7722 wants to merge 1 commit into
gustavodiaz7722 wants to merge 1 commit into
Conversation
gustavodiaz7722
force-pushed
the
feat/ensure-references
branch
2 times, most recently
from
August 28, 2026 01:53
c279560 to
c2f3d04
Compare
Member
Author
|
/retest |
knottnt
reviewed
Sep 1, 2026
gustavodiaz7722
force-pushed
the
feat/ensure-references
branch
from
September 2, 2026 17:47
c2f3d04 to
e4cb381
Compare
gustavodiaz7722
force-pushed
the
feat/ensure-references
branch
from
September 2, 2026 18:16
e4cb381 to
90115f5
Compare
A cross-resource reference (*Ref) is generated as a sibling of the concrete field it resolves into. A resource manager builds its return value from an AWS API response, which has no concept of a reference, so rebuilding the containing struct drops every *Ref inside it. That disables ClearResolvedReferences, which suppresses a resolved value only while the sibling *Ref is visible, so the spec patch deletes the declared *Ref and stores the resolved value in its place. The next apply of the manifest puts the *Ref back beside that value, a pair validateReferenceFields rejects, stopping reconciliation. Add an optional ReferenceEnsurer interface and invoke it on the object a resource manager hands back from Create and from Update, sourcing the references from the declared resource. It is kept separate from ReferenceManager and reached through a type assertion, so controllers generated before the method existed still satisfy AWSResourceManager and compile unchanged; they opt in by regenerating. Both paths now hand the resource manager a copy and keep `desired` as the reference source. Update already did this with reconcileDesired; Create was passing `desired` itself, and generated sdkCreate only deep-copies the resource it is given partway through -- a custom_implementation returns before that point and a sdk_create_pre_build_request hook runs before it -- so either could mutate what the user declared. The copy is taken after setResourceManaged and EnsureTags so it carries the finalizer and controller tags. The restoration runs before the error from Create or Update is inspected. A resource manager may hand back a non-nil resource alongside a requeue error while an asynchronous operation is in flight, and many do; that object reaches the caller either way, so it should carry the declared references on every path. The restoration is not hooked into patchResourceMetadataAndSpec because the late-initialization patch uses the AWS-observed object as its base, which carries no references. Which shapes are covered is a property of the generated method rather than of this interface. Pairs with aws-controllers-k8s/code-generator#738, which generates the method. Issue aws-controllers-k8s/community#2431 Issue aws-controllers-k8s/community#2361
gustavodiaz7722
force-pushed
the
feat/ensure-references
branch
from
September 2, 2026 19:00
90115f5 to
3e40e33
Compare
Contributor
|
/retest |
This was referenced Sep 8, 2026
ack-prow Bot
pushed a commit
that referenced
this pull request
Sep 8, 2026
Description of changes: `unit-test` is currently failing on every PR, including unmodified `main`. It fails in the `mocks` target, which `make test` depends on, so no test runs: ``` building mocks for pkg/types ... internal error: package "k8s.io/apimachinery/pkg/apis/meta/v1" without types was imported from "github.com/aws-controllers-k8s/runtime/pkg/types" make: *** [Makefile:24: mocks] Error 1 ``` `scripts/install-mockery.sh` builds mockery from source with whatever Go the CI image provides, and aws-controllers-k8s/test-infra#1084 moved `go_version` from 1.26.5 to 1.27.1 on 2026-09-02. mockery v2.53.3 pins `golang.org/x/tools v0.30.0`, whose `go/packages` predates Go 1.27 and cannot type-check its standard library, which is what produces the `without types` loader error. The timeline matches: the last `unit-test` run before the image bump passed (PR #267, 2026-09-02T19:04Z), and runs after it fail. v2.53.7 pins `golang.org/x/tools v0.49.0`, which handles Go 1.27. The mocks are regenerated so the committed output matches the new version. The only content changes are the generated-by header and import grouping — no mock behaviour changes. This also drops `mocks/pkg/types/resolved_reference_manager.go`. Its `ResolvedReferenceManager` interface no longer exists in `pkg/types`, so mockery does not generate it and nothing references it. It survived because the `mocks` target overwrites files rather than starting from a clean directory, so a `make clean-mocks && make mocks` cycle would otherwise always leave the tree dirty. ### Testing Reproduced and verified locally against both Go versions, building mockery from source exactly as the job does: | Go | mockery | `make mocks` | | --- | --- | --- | | 1.26.0 | v2.53.3 | passes | | 1.27.1 | v2.53.3 | fails with the error above | | 1.27.1 | v2.53.7 | passes | Under Go 1.27.1 with this change, all six mock sets generate and the full `go test ./...` passes. I also confirmed v2.53.3 fails on unmodified `main` under Go 1.27.1, so this is not specific to any open PR. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
knottnt
approved these changes
Sep 11, 2026
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: gustavodiaz7722, knottnt The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
Contributor
|
/retest |
1 similar comment
Member
Author
|
/retest |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A cross-resource reference (
*Ref) is generated as a sibling of the concrete field it resolves into —spec.vpcConfig.subnetRefsnext tospec.vpcConfig.subnetIDs. A resource manager builds its return value from an AWS API response, which has no concept of a reference, so rebuilding the containing struct drops every*Refinside it.That disables
ClearResolvedReferences, which suppresses a resolved value only while it can still see the sibling:So the spec patch deletes the declared
*Refand stores the resolved value in its place — what aws-controllers-k8s/community#2431 reports: a declaredsecurityGroupRefsreplaced bysecurityGroupIDs.Reconciliation continues until the manifest is applied again, from Helm, Argo, Flux or
kubectl apply. That apply restores the*Refbeside the now-stored value, andvalidateReferenceFieldsrejects the pair:This PR adds an optional
ReferenceEnsurerinterface and invokes it on the object a resource manager hands back fromCreateand fromUpdate, sourcing the references from the declared resource.Fixes aws-controllers-k8s/community#2431. Pairs with aws-controllers-k8s/code-generator#738, which generates the method.
Backwards compatible
ReferenceEnsureris deliberately separate fromReferenceManagerand reached through a type assertion, so every controller generated before the method existed still satisfiesAWSResourceManagerand compiles unchanged. Those controllers take the existing path untouched and opt in by regenerating.TestReconcilerUpdate_WithoutEnsurerIsUnaffectedpins that.Why these two call sites
The restoration runs where a manager returns an object whose spec is about to be patched back, not at
patchResourceMetadataAndSpec, because the source has to be the declared, reference-resolved resource — and that is only in scope on these paths.lateInitializeResourcepatches with the AWS-observedlatestas its base, so hooked into the shared patch path the restoration would have been handed a source carrying no references at all.TestReconcilerUpdate_LateInitializeIsNotAffectedByEnsureReferenceskeeps that boundary asserted.Why
desiredand notreconcileDesiredreconcileDesiredis what gets handed torm.Update, and a resource manager may mutate the object it is given.apigateway'sApiKeysdkUpdateassignsdesired.ko.Spec.StageKeysstraight from theUpdateApiKeyresponse (sdk.go:331), so by the timeUpdatereturns it is no longer a record of what the user declared.applyIgnoredFieldslikewise merges observed values into it for a resource carrying the ignore-field-drift annotation. Onlydesiredis clean, andTestReconcilerUpdate_EnsuresReferencesAfterUpdateasserts it is what gets passed.What the generated method does
Detailed in aws-controllers-k8s/code-generator#738. Summarised here because it bounds this PR's blast radius:
*Ref— nothing emitted; it cannot be lost, since every write path starts from aDeepCopyof the object it was handed.eks/cluster,lambda/functionandopensearchservice/domainalready hand-maintain insdk_*_post_set_outputhooks.So 117 struct-nested references across 37 resources in 25 controllers change behaviour; the 315 top-level and 38 list-nested ones are untouched.
Testing
Four tests in
pkg/runtime/reconciler_test.go:TestReconcilerUpdate_EnsuresReferencesAfterUpdateUpdate, sourced fromdesired, and the returned object is what gets patchedTestReconcilerCreate_EnsuresReferencesAfterCreateTestReconcilerUpdate_LateInitializeIsNotAffectedByEnsureReferencesTestReconcilerUpdate_WithoutEnsurerIsUnaffectedFull runtime suite passes. Verified on a cluster against real generated code (
lambdaandec2regenerated against code-generator#738): aFunctiondeclaringvpcConfig.securityGroupRefs/subnetRefskeeps both in its stored spec through create and across repeated resyncs, with no resolved IDs written and noReference resolution failedcondition.Not addressed
The read path. This runs after
Createand afterUpdate, not afterReadOne. Two spec writes consume aReadOne-derived object and so can still delete a nested*Ref: theAdoptionPolicy_Adoptbranch ofSync, anddeleteResource. Both use the stored CR as the patch base and theReadOneresult as the target. Confirmed on a cluster for the adoption path, which is why the existingsdk_read_one_post_set_outputhooks in the three controllers above must stay. Covering it is a separate change: the unresolveddesiredon those paths is a valid source, since a*Refis user-declared and resolution only fills the concrete sibling.The delta. It is computed against the raw
ReadOneresult, which this does not touch. A dropped struct-nested*Refwas invisible to the delta — the generated delta compares*Reffields only at the top level — so it never drove a redundantUpdateand there is nothing left to fix for the shapes this PR covers.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.