Skip to content

fix: correct two custom_sync behaviors found while adopting it - #734

Open
gustavodiaz7722 wants to merge 2 commits into
aws-controllers-k8s:mainfrom
gustavodiaz7722:fix/custom-sync-allow-compare-ignored
Open

fix: correct two custom_sync behaviors found while adopting it#734
gustavodiaz7722 wants to merge 2 commits into
aws-controllers-k8s:mainfrom
gustavodiaz7722:fix/custom-sync-allow-compare-ignored

Conversation

@gustavodiaz7722

@gustavodiaz7722 gustavodiaz7722 commented Aug 14, 2026

Copy link
Copy Markdown
Member

Two corrections to custom_sync (#732), found while adopting it in eventbridge-controller.

1. Allow custom_sync with compare.is_ignored

#732 rejected the combination:

resources.Rule.fields.Targets.custom_sync: cannot be combined with compare.is_ignored,
because the field would never appear in the delta and the resource would stop reconciling

The premise is wrong. compare.is_ignored suppresses only the generated comparison. A resource with a delta_pre_compare hook adds the same path by hand, and that is exactly how the out-of-band field pattern custom_sync generalizes is written in the controllers that have it today. eventbridge Rule ignores both Spec.Tags and Spec.Targets and adds each in customPreCompare:

if !pkgtags.EqualTags(desired.ko.Spec.Tags, latest.ko.Spec.Tags) {
    delta.Add("Spec.Tags", desired.ko.Spec.Tags, latest.ko.Spec.Tags)
}
if !equalTargets(desired.ko.Spec.Targets, latest.ko.Spec.Targets) {
    delta.Add("Spec.Targets", desired.ko.Spec.Targets, latest.ko.Spec.Targets)
}

Both paths are in the delta, so DifferentAt fires and DifferentExcept behaves. The check fired on the feature's most likely consumers, and the only way to adopt custom_sync was to drop is_ignored and rework the resource's whole comparison — which has nothing to do with the field being synced out of band. For a set-like list of structs that also means hand-writing an ordering normalization, since the generated comparison for that shape is a whole-slice DeepEqual.

Removed rather than narrowed. Whether a path reaches the delta is a property of hand-written code the generator cannot see, so every variant of the check is a guess: "ignored and no delta_pre_compare hook" would still be one, because a hook's existence does not prove it adds this path. The failure mode it guarded is local and visible — the field stops being applied — which is the same failure mode as a missing sync<Field> method, and that is not pre-validated either.

2. Add custom_sync.applied_on_create

The post-create marker assumes a custom_sync field is applied only in the update path, so it sets Synced=false after create to force a requeue.

That is wrong for a field the Create input shape carries — and it is wrong for both known consumers. newCreateRequestPayload writes Tags into the request for eventbridge Rule (PutRule) and autoscaling AutoScalingGroup (CreateAutoScalingGroup) alike:

eventbridge  res.Tags = f7      (PutRuleInput)
autoscaling  res.Tags = f27     (CreateAutoScalingGroupInput)

So tags are live the moment create returns. The marker costs a needless reconcile and reports the resource as not synced while it is, for up to requeue.DefaultRequeueAfterDuration.

Tags still need custom_sync, because tag updates go through TagResource/UntagResource rather than the Update operation. Both facts now coexist:

Tags:
  custom_sync:
    applied_on_create: true

The option governs the post-create marker only. The field keeps its place in the update path — sdkUpdate still calls its sync function when the delta reports it, and still counts it in the DifferentExcept short-circuit. A resource whose every custom_sync field is applied_on_create emits no marker at all.

Why a bool, not an operation selector

The axis that varies is only whether create leaves the field pending; the sync function is always called from update. An operations: [Create, Update] shape would imply you can request a create-path sync the generator does not emit, and would inherit set.method's unvalidated-free-string behavior (method: Read matches nothing, silently). CustomSyncConfig stays a struct, so an operations: key remains addable later without breaking the files adopting this now — which matters, because eventbridge Targets needs delete-path teardown and that is the case that would genuinely justify a selector.

Why author-stated rather than derived

The generator can see whether SetSDK puts a field in the create input, so it could derive this with no config at all. It should not: the marker is a claim about AWS runtime behavior ("this field isn't live yet") while the generator only knows wire shape ("the field was in the request struct"). Those diverge exactly in the interesting case — an API that accepts a field on create and ignores it — and there derivation silently drops a marker that was needed. ACK already treats this class of claim as an assertion; is_immutable, late_initialize and tags.ignore are none of them verifiable either.

Naming: applied_on_create states the API fact. skip_create_marker would name the emitted code, and go stale if the marker's implementation changes.

Validation

Config Rejected
custom_sync on a nested field yes, unchanged
custom_sync with is_read_only yes, unchanged
custom_sync with no Update operation yes, unchanged
custom_sync with compare.is_ignored no longer
applied_on_create with no Create operation new

Testing

make test passes — 15 packages, no failures.

  • TestCustomSyncInvalid_CompareIgnored becomes TestCustomSync_CompareIgnored, asserting the config is accepted and honored (field still collected, still yields syncTags). Keeping the fixture and inverting the assertion pins the new behavior instead of deleting the coverage.
  • New TestCustomSyncCreate_AppliedOnCreate asserts the marker narrows to the still-pending field, and TestCustomSyncUpdate_AppliedOnCreate asserts the update path is untouched — that second one is the regression that would actually hurt, since a tags-only change falling through to the Update operation cannot be applied by it.
  • New model tests cover the accessors and the default (custom_sync: {} stays pending after create).
  • New fixture generator-with-custom-sync-applied-on-create.yaml.

Verified end to end by regenerating eventbridge-controller with Tags: custom_sync: {applied_on_create: true} and Targets: custom_sync: {}. The marker narrows to the one field create does not apply, while both fields keep their update-path sync:

// sdkCreate
if ko.Spec.Targets != nil {
	msg := "Secondary sync required; resource will be requeued"
	ackcondition.SetSynced(&resource{ko}, corev1.ConditionFalse, &msg, nil)
}

// sdkUpdate
if delta.DifferentAt("Spec.Tags")    { err = rm.syncTags(ctx, desired, latest) ... }
if delta.DifferentAt("Spec.Targets") { err = rm.syncTargets(ctx, desired, latest) ... }
if !delta.DifferentExcept("Spec.Tags", "Spec.Targets") { ... }

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

The check rejecting `custom_sync` on a `compare.is_ignored` field claimed
the field would never appear in the delta, so the sync would never run and
the generated DifferentExcept short-circuit would fire on every reconcile.

That is not true. `compare.is_ignored` suppresses only the GENERATED
comparison. A resource with a `delta_pre_compare` hook adds the same path
by hand, and that is precisely how the out-of-band field pattern
`custom_sync` generalizes is written in the controllers that have it
today - eventbridge Rule ignores both Spec.Tags and Spec.Targets and adds
each in customPreCompare. So the check fired on the feature's most likely
consumers, and the only way to adopt `custom_sync` was to drop
`is_ignored` and rework the resource's comparison, which is unrelated to
the field being synced out of band.

Remove it rather than narrow it. Whether a path reaches the delta is a
property of hand-written code the generator cannot see, so every variant
of this check is a guess: "ignored AND no delta_pre_compare hook" would
still be one, since a hook's existence does not prove it adds THIS path.
The runtime symptom of getting it wrong is local and visible - the field
stops being applied - which is the same failure mode as an unimplemented
sync method, and that is not pre-validated either.

The three remaining validations are unaffected: nested field, is_read_only,
and no Update operation all stay rejected, since each is decidable from
the config alone.

The fixture that asserted the rejection now asserts the config is honored,
so the field is still collected and still yields sync<Field>.
@ack-prow

ack-prow Bot commented Aug 14, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: gustavodiaz7722
Once this PR has been reviewed and has the lgtm label, please assign michaelhtm for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@ack-prow
ack-prow Bot requested review from a-hilaly and knottnt August 14, 2026 21:11
The post-create marker assumes a `custom_sync` field is applied only in the
update path, so after a successful create it sets Synced=false to make the
runtime requeue and apply the field.

That is wrong for a field the Create input shape carries, and it is wrong
for both known consumers of the feature. `newCreateRequestPayload` writes
Tags into the request for eventbridge Rule (PutRule) and for autoscaling
AutoScalingGroup (CreateAutoScalingGroup) alike, so tags are live the
moment create returns. The marker costs a needless reconcile and, worse,
reports the resource as not synced while it is - for up to
requeue.DefaultRequeueAfterDuration.

Tags still need `custom_sync`, because tag UPDATES go through
TagResource/UntagResource rather than the Update operation. So the two
facts have to be expressible at once:

  Tags:
    custom_sync:
      applied_on_create: true

The option governs the post-create marker ONLY. The field keeps its place
in the update path: sdkUpdate still calls its sync function when the delta
reports it, and still counts it in the DifferentExcept short-circuit. A
resource whose every custom_sync field is applied_on_create emits no marker
at all.

Stated by the author rather than derived from the Create input shape. The
generator can see that a field was written into the request struct but not
whether AWS acted on it, so an API that accepts a field on create and
ignores it would silently lose its marker and report Synced on a field that
was never applied. ACK already treats this class of claim as an assertion:
is_immutable, late_initialize and tags.ignore are none of them verifiable
either.

A bool rather than an operation selector, because the axis that varies is
only whether create leaves the field pending - the sync function is always
called from update. CustomSyncConfig stays a struct, so an `operations:`
key remains addable later without breaking the files adopting this now.

Rejected at generation time: `applied_on_create` on a resource with no
Create operation, which would drop the marker on the claim of an operation
that does not exist.
@gustavodiaz7722 gustavodiaz7722 changed the title fix: allow custom_sync with compare.is_ignored fix: correct two custom_sync behaviors found while adopting it Aug 14, 2026
@gustavodiaz7722

Copy link
Copy Markdown
Member Author

/retest

@ack-prow

ack-prow Bot commented Aug 15, 2026

Copy link
Copy Markdown

@gustavodiaz7722: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ec2-controller-test 659fd22 link true /test ec2-controller-test

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant