Use lazy copy to patch resources to ensure multiple modifications are applied to the base resource - #4580
Conversation
|
Hello! Thank you for your contribution. Please review our contribution guidelines to understand the project's testing and code conventions. |
f43f84b to
ab1c70d
Compare
There was a problem hiding this comment.
Pull request overview
This PR modernizes how the controllers detect/propagate desired-state changes and how they compute patches, replacing integrity-hash annotations with explicit revision/generation tracking and adding a shared in-memory resource cache to avoid rebuilding identical desired objects.
Changes:
- Introduces a
ResourceCacheused byResourceBuilderto reuse desired objects based on a main-object key plus dependency refs. - Replaces integrity-hash–based update detection with
ActionableRevision/AppliedActionableRevisionforEphemeralRunnerSetandObservedGenerationforAutoscalingRunnerSet(and updates CRDs accordingly). - Refactors patch flows to use a lazy DeepCopy helper (
once) so multiple in-place mutations can be safely applied beforeclient.MergeFrom(...).
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| main.go | Instantiates and wires a shared ResourceCache into ResourceBuilder. |
| controllers/actions.github.com/utils.go | Adds once lazy-copy helper used to build correct MergeFrom patches. |
| controllers/actions.github.com/utils_test.go | Moves test-only random string helper into tests. |
| controllers/actions.github.com/resourcecache.go | Adds typed resource cache with dependency-key tracking and eviction by owner UID. |
| controllers/actions.github.com/resourcecache_test.go | Adds unit tests for cache semantics (dependency ordering, deletion, invalid inputs). |
| controllers/actions.github.com/resourcebuilder.go | Uses cache for desired-object reuse; removes integrity-hash annotations; refactors label/annotation merging. |
| controllers/actions.github.com/resourcebuilder_test.go | Updates tests for removed integrity-hash annotation; adds merge-map tests and cache expectations. |
| controllers/actions.github.com/helpers.go | Adds helper predicates for actionable revision and pod recreation decisions. |
| controllers/actions.github.com/ephemeralrunnerset_controller.go | Switches to actionable revision + status patching with conflict retries; refactors patching with lazy copies. |
| controllers/actions.github.com/ephemeralrunnerset_controller_test.go | Updates/extends integration tests for cache cleanup and actionable revision behavior. |
| controllers/actions.github.com/ephemeralrunner_controller.go | Refactors finalizer patching to use lazy-copy pattern; deletes cached entries on deletion. |
| controllers/actions.github.com/ephemeralrunner_controller_test.go | Extends tests to assert cache cleanup on runner deletion. |
| controllers/actions.github.com/autoscalingrunnerset_controller.go | Uses observed generation for Pending detection; switches spec-change handling to actionable revision; refactors patch logic. |
| controllers/actions.github.com/autoscalingrunnerset_controller_test.go | Updates tests to assert observed generation and actionable revision behavior; adds cache assertions. |
| controllers/actions.github.com/autoscalinglistener_controller.go | Refactors patching of dependent resources using lazy copies; updates pod recreation decision logic. |
| controllers/actions.github.com/autoscalinglistener_controller_test.go | Extends tests to assert resources are cached and evicted appropriately. |
| config/crd/bases/actions.github.com_ephemeralrunnersets.yaml | Adds actionableRevision, appliedActionableRevision, finishedRunnerCleanupPatchID schema fields. |
| config/crd/bases/actions.github.com_autoscalingrunnersets.yaml | Adds observedGeneration to status schema. |
| charts/gha-runner-scale-set-controller/crds/actions.github.com_ephemeralrunnersets.yaml | Mirrors ERS CRD schema additions into chart CRDs. |
| charts/gha-runner-scale-set-controller/crds/actions.github.com_autoscalingrunnersets.yaml | Mirrors ARS CRD schema additions into chart CRDs. |
| charts/gha-runner-scale-set-controller-experimental/crds/actions.github.com_ephemeralrunnersets.yaml | Mirrors ERS CRD schema additions into experimental chart CRDs. |
| charts/gha-runner-scale-set-controller-experimental/crds/actions.github.com_autoscalingrunnersets.yaml | Mirrors ARS CRD schema additions into experimental chart CRDs. |
| apis/actions.github.com/v1alpha1/version.go | Minor condition reordering in version allowance check. |
| apis/actions.github.com/v1alpha1/ephemeralrunnerset_types.go | Adds ActionableRevision spec field and applied/cleanup status fields. |
| apis/actions.github.com/v1alpha1/autoscalingrunnerset_types.go | Adds ObservedGeneration to status. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
7b5e00f to
fdb0651
Compare
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Immutable RoleBinding and Secret fields are patched in place, which can leave reconciliation permanently failing.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review tier: Balanced
Findings: 1
New issues introduced by this change (2)
| Severity | Finding |
|---|---|
controllers/actions.github.com/autoscalinglistener_controller.go — A Secret's immutable flag cannot be unset after it becomes true. For an immutable live proxy… |
|
controllers/actions.github.com/autoscalinglistener_controller.go — RoleBinding.roleRef is immutable after creation. If this comparison ever detects drift, the… |
Issues resolved since last review (3)
| Severity | Finding |
|---|---|
controllers/actions.github.com/utils.go — The panic message in (*once).Get() is very generic. Using a more specific message will make… View resolved comment |
|
controllers/actions.github.com/autoscalinglistener_controller.go — rulesModified is misleading here because this block is checking/modifying RoleRef, not RBAC… View resolved comment |
|
controllers/actions.github.com/autoscalingrunnerset_controller.go — Typo in the variable name listnerLabelsModified makes the code harder to read/search. Rename it… View resolved comment |
Suppressed comments (1)
controllers/actions.github.com/autoscalinglistener_controller.go:459
- A Secret's
immutableflag cannot be unset after it becomes true. Setting it to nil guarantees this patch is rejected for an immutable config Secret, leaving reconciliation stuck; preserve it when data is unchanged, or delete and recreate the Secret when the config changes.
if listenerConfigSecret.Immutable != nil {
original.Do()
listenerConfigSecret.Immutable = nil
}
The merge-base changed after approval.
fdb0651 to
a143937
Compare
c73a1a0 to
bf86a32
Compare
bf86a32 to
7cc4c1d
Compare
7cc4c1d to
953e246
Compare
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The implementation is coherent and tested; the identified documentation correction is non-blocking.
Review tier: Balanced
Findings: 1
New issues introduced by this change (1)
| Severity | Finding |
|---|---|
controllers/actions.github.com/lazycopy.go — Do not claim mutation ordering is enforced |
Pre-existing issues (2)
| Severity | Finding |
|---|---|
controllers/actions.github.com/autoscalinglistener_controller.go — A Secret's immutable flag cannot be unset after it becomes true. For an immutable live proxy… View comment |
|
controllers/actions.github.com/autoscalinglistener_controller.go — RoleBinding.roleRef is immutable after creation. If this comparison ever detects drift, the… View comment |
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The implementation is consistent and tested; only a non-blocking documentation correction remains.
Review tier: Balanced
Findings: None
Issues resolved since last review (3)
| Severity | Finding |
|---|---|
controllers/actions.github.com/lazycopy.go — Do not claim mutation ordering is enforced View resolved comment |
|
controllers/actions.github.com/autoscalinglistener_controller.go — RoleBinding.roleRef is immutable after creation. If this comparison ever detects drift, the… View resolved comment |
|
controllers/actions.github.com/autoscalinglistener_controller.go — A Secret's immutable flag cannot be unset after it becomes true. For an immutable live proxy… View resolved comment |
The merge-base changed after approval.
Reconcilers deep copied the object they had just fetched on every single reconcile, purely so a merge patch could be computed on the rare pass that actually changes something. The copy is a full recursive walk and allocation of the object, and the overwhelming majority of reconciles throw it away untouched. Introduce lazyCopy, which takes the snapshot on the first call to Mutate and hands back the live object. Because Mutate is the only way to reach the object, the snapshot cannot be taken after the mutation it is supposed to be diffed against, which is the way this optimization is usually gotten wrong. Apply it to the four Reconcile entry points, and move the two EphemeralRunnerSet status copies inside the branch that patches, so they are only paid for when the status really changed. While here, drop the short circuit in the EphemeralRunner finalizer block: `addedFinalizers || AddFinalizer(...)` skipped adding the actions finalizer whenever the first finalizer was added. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
lazycopy.go introduces deepCopyObject, a generic type constraint rather than a collaborator, and the package's "all: true" mockery config picks it up and emits a large mock that nothing can use. Switch the package to an explicit include/exclude regex pair so every other interface is still discovered automatically while deepCopyObject is skipped. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
953e246 to
9b4c03e
Compare



Based on #4575