Skip to content

test(vpc): add e2e coverage for ignore-field-drift - #361

Open
sapphirew wants to merge 5 commits into
aws-controllers-k8s:mainfrom
sapphirew:test/vpc-ignore-field-drift-e2e
Open

test(vpc): add e2e coverage for ignore-field-drift#361
sapphirew wants to merge 5 commits into
aws-controllers-k8s:mainfrom
sapphirew:test/vpc-ignore-field-drift-e2e

Conversation

@sapphirew

@sapphirew sapphirew commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Description

Adds e2e integration tests exercising the services.k8s.aws/ignore-field-drift annotation (aws-controllers-k8s/runtime#256, released in runtime v0.62.0) against the EC2 VPC resource.

EC2 is one of the controllers the runtime presubmit (runtime_presubmit_services) regenerates and e2e-tests, so this coverage runs on runtime PRs and guards the feature against regressions.

The IgnoreFieldDrift feature gate is Alpha and disabled by default, so the tests self-enable it on the deployed controller by patching the deployment's FEATURE_GATES env var and waiting for rollout.

That patch rolls the shared controller Deployment, which is a cluster-wide side effect in a suite running 32 xdist workers — a restart mid-run can time out any other worker waiting on ACK.ResourceSynced. The gate is therefore turned on once per run and never turned back off (check-then-set, so concurrent workers produce a byte-identical pod template and no second rollout; no restore, so a teardown in one worker can't disable the gate underneath a drift test still running in another). Leaving it on is safe because the gate is inert unless a resource carries the annotation, and only this file's resources do.

Follow-up to remove the remaining rollout entirely: set IgnoreFieldDrift in FEATURE_GATES at controller setup time in test-infra scripts/controller-setup.sh, alongside the existing IAMRoleSelector case, after which this fixture degrades to a no-op check.

What the tests assert

Four cases, covering the field shapes and drift sources that matter:

test_tags_drift_ignored — collection field, spec.tags ignored:

  • the declared tag is applied at create;
  • an externally-added tag survives — the controller does not call DeleteTags for drift on the ignored field;
  • the resource stays Synced even though spec.tags differs from the live AWS tag set;
  • an edit to spec.tags is retained in the CR spec but is not pushed to AWS.

test_scalar_external_drift_ignored — scalar leaf, spec.enableDNSSupport ignored. An external ModifyVpcAttribute flip survives and the resource stays Synced.

test_scalar_spec_edit_not_pushed — the same scalar, with the edit made while AWS still holds the other value, so declining to push it is observable. Separate test rather than a second half of the one above: the field is boolean, so after an external flip spec and AWS agree and the assertion would be vacuous. It also covers a different code path — enableDNSSupport is pushed by ModifyVpcAttribute in the custom update path, not by tags.Sync.

test_declared_tag_value_drift_ignored — an externally-changed value on a tag key the CR declares. Distinct from an added key: this is the case tags.Sync would actively revert.

Note none of these exercise the runtime's path-prefix match. Every Delta path here equals its annotation path — pkg/resource/vpc/delta.go compares tags as a whole and emits a single delta.Add("Spec.Tags", ...), so spec.tags matches by segment equality. Prefix matching needs a Delta subject deeper than the annotation, which none of VPC's ignorable fields produce.

Testing

Ran locally via make kind-test SERVICE=ec2 against released runtime v0.62.0 — no leaked VPCs or CRs (teardown fixture verified clean).

Files

  • test/e2e/tests/test_vpc_ignore_field_drift.py
  • test/e2e/resources/vpc_ignore_field_drift.yaml

Note on the third commit

aa14cd9 chore: regenerate apis/v1alpha1/types.go is unrelated to the tests and is here to get ec2-verify-code-gen green.

code-generator#733 (d13ed56, Aug 15) removed the forced aws.JSONValue import from templates/apis/types.go.tpl. It is not in a release yet — latest is v0.62.1, cut two days earlier — but the presubmit regenerates with code-generator main, so the committed types.go (generated at v0.62.1, per apis/v1alpha1/ack-generate-metadata.yaml) reads as drifted:

-	"github.com/aws/aws-sdk-go/aws"
-	_ = &aws.JSONValue{}

The commit applies exactly that. aws-sdk-go v1 stays in go.modpkg/resource/*/hooks.go still uses it — so nothing else changes. This is not specific to this PR: main carries the stale hack, so any ec2-controller PR whose presubmit runs now hits it (s3-controller and iam-controller are in the same state; rds-controller never had the hack, which is why its PRs stay green). Happy to split this into its own PR if you'd rather.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@ack-prow
ack-prow Bot requested review from a-hilaly and michaelhtm July 28, 2026 23:45
@ack-prow ack-prow Bot added the approved label Jul 28, 2026
@sapphirew

Copy link
Copy Markdown
Contributor Author

Heads up: I pushed a temporary commit (41ed2d8, titled DO NOT MERGE) that adds a go.mod replace pointing the ACK runtime at my fork branch (runtime PR #256, the IgnoreFieldDrift gate).

Why: the ec2-kind-e2e check builds the controller from the checked-in go.mod, which pins the released runtime v0.61.0. That runtime doesn't know the IgnoreFieldDrift gate, and gate parsing is strict — the controller os.Exit(1)s at startup on FEATURE_GATES=IgnoreFieldDrift=true, so the new tests fail with "did not roll out". The replace lets CI build a controller that recognizes the gate and actually exercise the tests once.

Merge plan: runtime #256 merges & releases first → ec2-controller bumps to a runtime version containing the gate → I revert 41ed2d8 → this PR merges. The replace commit must not merge as-is.

@sapphirew

Copy link
Copy Markdown
Contributor Author

/hold

@ack-prow ack-prow Bot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Jul 29, 2026
@sapphirew
sapphirew force-pushed the test/vpc-ignore-field-drift-e2e branch from 41ed2d8 to 14e9fde Compare August 25, 2026 22:15
@sapphirew sapphirew changed the title test(vpc): add e2e coverage for ignore-field-drift on spec.tags test(vpc): add e2e coverage for ignore-field-drift Aug 25, 2026
@sapphirew
sapphirew force-pushed the test/vpc-ignore-field-drift-e2e branch from aa14cd9 to 378f688 Compare August 25, 2026 22:34
@sapphirew

Copy link
Copy Markdown
Contributor Author

/unhold

@ack-prow ack-prow Bot removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Aug 25, 2026
@sapphirew

Copy link
Copy Markdown
Contributor Author

/retest

2 similar comments
@sapphirew

Copy link
Copy Markdown
Contributor Author

/retest

@sapphirew

Copy link
Copy Markdown
Contributor Author

/retest

@sapphirew
sapphirew force-pushed the test/vpc-ignore-field-drift-e2e branch from 3214995 to b2cb92a Compare August 26, 2026 22:18
@sapphirew

Copy link
Copy Markdown
Contributor Author

/retest

2 similar comments
@sapphirew

Copy link
Copy Markdown
Contributor Author

/retest

@sapphirew

Copy link
Copy Markdown
Contributor Author

/retest

Comment on lines +419 to +425
def test_nested_field_under_ignored_parent(
self, ec2_client, ignore_field_drift_enabled, ignore_field_drift_vpc,
):
"""Drift on a nested child of an ignored parent: the declared tag's
value (Delta path Spec.Tags.N.Value) changes externally while only the
parent spec.tags is ignored. The runtime must match this by path prefix,
so the child drift is ignored too."""

@gustavodiaz7722 gustavodiaz7722 Aug 27, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gen AI generated comment.

  • Issue: The test claims to exercise the runtime's path-prefix match with a Delta path of Spec.Tags.N.Value under an ignored spec.tags. No such path exists. pkg/resource/vpc/delta.go compares tags as a whole via convertToOrderedACKTags + MapStringStringEqual and emits exactly one difference, delta.Add("Spec.Tags", ...). So Path.ContainsFold("spec.tags") matches by exact segment equality here — the same branch test_tags_drift_ignored already covers. Two independent confirmations: runtime/pkg/compare/path_test.go asserts Path("Spec.KMSKeyID").ContainsFold("spec.kmsKeyID.extra") is False, i.e. Contains requires the ignored path to be a prefix of the Delta path, so prefix matching needs a Delta path deeper than the annotation; and isValidFieldPath in ignore_field_drift.go rejects indices outright, so Spec.Tags.N.Value is not even expressible as an annotation.
  • Fix: Either (a) pick a field whose generated delta genuinely emits a nested path — a nested struct member where code-gen produces a compareX helper with a Spec.Parent.Child subject — and annotate the parent; or (b) drop the prefix-match claim. Option (b) is fine: tag-value drift on a declared key is a materially different case from a new externally-added key (it is the one that tags.Sync would actively revert), so keep the test but rename it to test_declared_tag_value_drift_ignored and correct the docstring plus the PR description. Do not leave a test asserting coverage of a branch it never reaches.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and taken option (b). pkg/resource/vpc/delta.go:143-146 emits a single delta.Add("Spec.Tags", ...) via convertToOrderedACKTags + MapStringStringEqual, and Path.Contains (runtime/pkg/compare/path.go:63-77) bails when the subject has more segments than the path, so spec.tags against Spec.Tags is segment equality — the branch test_tags_drift_ignored already reaches.

Renamed to test_declared_tag_value_drift_ignored in 13e3586. The docstring now states what it covers (a changed value on a declared key, which tags.Sync would actively revert, unlike an added key) and explicitly what it does not (prefix matching needs a Delta subject deeper than the annotation; per-element paths are not expressible since isValidFieldPath rejects indices). Corrected the class docstring, which repeated the same claim, and the PR description.

Comment on lines +405 to +412
# Editing the ignored scalar in the spec is retained but NOT pushed to
# AWS: patch to a new value and confirm the live attribute is unchanged.
k8s.patch_custom_resource(ref, {"spec": {"enableDNSSupport": True}})
time.sleep(MODIFY_WAIT_AFTER_SECONDS)

assert _dns_support_enabled(ec2_client, vpc_id) is True
latest = k8s.get_resource(ref)
assert latest["spec"].get("enableDNSSupport") is True

@gustavodiaz7722 gustavodiaz7722 Aug 27, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gen AI generated comment.

  • Issue: The "edit retained but not pushed" assertions are vacuous. The external actor has already set the live attribute to True via modify_vpc_attribute; the test then patches spec.enableDNSSupport to True and asserts the live attribute is True and the spec is True. Both hold whether or not the edit was pushed — spec and AWS agree, so there is no delta to suppress and nothing being tested. Contrast test_tags_drift_ignored, which patches to team=changed while AWS holds payments, and is therefore sound.
  • Fix: Reorder so the edit is made against a differing live value. Create with enableDNSSupport: False (the controller applies it at create), patch the spec to True, assert the live attribute is still False and spec.enableDNSSupport is True — that is the real retain/not-pushed assertion. Then do the external-flip half. Because the field is boolean there is no third value, so ordering is the only way to make both halves non-vacuous.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed vacuous — the external flip sets the live value to True before the spec is patched to True, so the assertion holds either way.

One correction to the proposed fix: reordering within a single test does not resolve it, because the field is boolean. After create False → patch spec True, asserting the live value is still False is sound; but then flipping live to True makes spec and AWS agree, so the external-drift half asserts nothing. Whichever half runs second finds no delta to suppress, and there is no third value to escape with.

Split into two tests in 13e3586, each with its own VPC:

  • test_scalar_external_drift_ignored — create False, external flip to True, assert it survives and stays Synced.
  • test_scalar_spec_edit_not_pushed — create False, patch spec to True, assert the live value is still False and the spec retains True.

The edit case also earns a separate test on its own merits: enableDNSSupport reaches AWS through ModifyVpcAttribute in the custom update path rather than tags.Sync, so suppression has to hold for that path too.

Comment on lines +256 to +260
try:
_, deleted = k8s.delete_custom_resource(ref, 3, 10)
assert deleted
except:
pass

@gustavodiaz7722 gustavodiaz7722 Aug 27, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gen AI generated comment.

  • Issue: except: pass around assert deleted silently discards teardown failure, so a leaked VPC or CR passes unnoticed — which directly undercuts the PR description's claim that teardown was verified clean. The bare form also swallows KeyboardInterrupt and SystemExit.
  • Fix: Remove the try/except and let assert deleted fail the teardown, or at minimum narrow to except Exception:, log, and re-raise. Separately, move the delete_tags cleanup of the out-of-band external tag out of the test body into the fixture teardown (or a finally), so a mid-test assertion failure does not leave it behind.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both points fixed in 13e3586. The try/except: pass is gone, so a failed delete fails the teardown, and the out-of-band external tag cleanup moved into a finally — as a trailing statement it was skipped whenever an assertion above it failed.

One addition: the delete budget goes from 30s to 60s in the same change. delete_custom_resource(ref, 3, 10) gives a VPC CR 30s to clear its finalizer, and ec2's own test_vpc.py::TestVpc::test_crud_tags failed on exactly assert deleted is True in a run on this PR earlier this week. Asserting on the old budget would have traded a silent leak for a flake, so the assertion now runs against a budget that reflects observed deletion times.

@sapphirew

Copy link
Copy Markdown
Contributor Author

/retest

@gustavodiaz7722

Copy link
Copy Markdown
Member

/LGTM

@ack-prow ack-prow Bot added the lgtm Indicates that a PR is ready to be merged. label Aug 28, 2026
@ack-prow

ack-prow Bot commented Aug 28, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: gustavodiaz7722, sapphirew

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

The pull request process is described here

Details Needs approval from an approver in each of these files:
  • OWNERS [gustavodiaz7722,sapphirew]

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

@sapphirew

Copy link
Copy Markdown
Contributor Author

/retest

3 similar comments
@sapphirew

Copy link
Copy Markdown
Contributor Author

/retest

@sapphirew

Copy link
Copy Markdown
Contributor Author

/retest

@sapphirew

Copy link
Copy Markdown
Contributor Author

/retest

Adds an integration test exercising the services.k8s.aws/ignore-field-drift
annotation (aws-controllers-k8s/runtime#256) against the EC2 VPC resource.
The IgnoreFieldDrift feature gate is Alpha and disabled by default, so the
test self-enables it on the deployed controller for the module and restores
the prior FEATURE_GATES value afterwards.

EC2 is one of the controllers the runtime presubmit regenerates and e2e-tests,
so this coverage runs on runtime PRs.

The test asserts, on a VPC annotated to ignore spec.tags:
- the declared tag is applied at create;
- an externally-added tag survives (controller does not call DeleteTags);
- the resource stays Synced despite tag drift;
- an edit to spec.tags is retained in the spec but not pushed to AWS.
sapphirew and others added 4 commits September 4, 2026 14:46
Extends the ignore-field-drift e2e coverage beyond the list-of-objects case
(spec.tags) to the two other field shapes the runtime treats differently:

- test_scalar_field_drift_ignored: ignore a scalar leaf
  (spec.enableDNSSupport) whose Delta path matches the ignored path exactly.
  Asserts an external modify_vpc_attribute flip is not reverted and a spec
  edit is retained but not pushed.
- test_nested_field_under_ignored_parent: drift on a nested child
  (a tag's value, Delta path Spec.Tags.N.Value) while only the parent
  spec.tags is ignored, exercising the runtime's path-prefix match rather
  than exact equality.

The VPC fixture is now parametrizable (indirect) on the ignored paths and the
baseline enableDNSSupport value; the existing test keeps its default behaviour.
The suite runs 32 pytest-xdist workers with LoadScheduling, which spreads
individual tests across worker PROCESSES. A fixture is therefore created
once in every worker that picks up a test from this file, at any scope --
so the module-scoped enable/restore pair ran three times in one run (the
three drift tests landed on gw31, gw9 and gw30), rolling the shared
ack-ec2-controller Deployment up to six times.

Each rollout stops reconciliation cluster-wide for a few seconds, which
is enough to fail an unrelated worker mid-assertion. Build 2092380219
lost three tests that way while all three drift tests passed:

  TestTGW::test_create_delete            KeyError: 'transitGatewayID'
  TestTGW::test_crud_tags                KeyError: 'transitGatewayID'
  TestSecurityGroup::
    test_self_ref_rule_no_perpetual_diff ACK.ResourceSynced timed out

Both shapes are "the controller was not reconciling during my window":
a status that never got populated, and a synced-condition wait that
expired.

Turn the gate on once and leave it on:

  - Check-then-set, so a worker that finds it already on skips the patch.
    Concurrent workers observing it off compute the same FEATURE_GATES
    value from the same starting point, so the second patch leaves the pod
    template identical, does not bump the Deployment generation and does
    not roll a second time. No cross-process lock needed.
  - No restore, which would cost a second rollout and could disable the
    gate underneath a drift test still running in another worker. Safe to
    leave on: the gate is inert unless a resource carries the
    ignore-field-drift annotation, and only this file's resources do.

Still one rollout per run. Removing it entirely means setting the gate at
controller setup time (FEATURE_GATES in test-infra's controller-setup.sh,
alongside the existing IAMRoleSelector case), after which this fixture
degrades to a no-op check. Filed as a follow-up.
Three review findings, all confirmed against the code.

1. test_nested_field_under_ignored_parent claimed to exercise the runtime's
   path-PREFIX match via a Delta path of Spec.Tags.N.Value. No such path
   exists. pkg/resource/vpc/delta.go compares tags as a whole
   (convertToOrderedACKTags + MapStringStringEqual) and emits a single
   delta.Add("Spec.Tags", ...), and Path.Contains requires the ignored path
   to be a prefix of the Delta path, so spec.tags vs Spec.Tags is segment
   equality -- the branch test_tags_drift_ignored already covers.
   Per-element paths are not expressible as an annotation either, since
   isValidFieldPath rejects indices.

   The scenario is still worth covering: a changed value on a DECLARED key
   is what tags.Sync would actively revert, unlike an externally-added key.
   Renamed to test_declared_tag_value_drift_ignored and the docstring now
   says what it does and does not cover. Also corrected the class docstring,
   which repeated the prefix-match claim.

2. The "edit retained but not pushed" half of test_scalar_field_drift_ignored
   was vacuous. The external actor had already set the live attribute to
   True, so patching spec.enableDNSSupport to True and asserting the live
   value is True holds whether or not the edit was pushed.

   Split into test_scalar_external_drift_ignored (external flip survives)
   and test_scalar_spec_edit_not_pushed (edit made while AWS still holds
   False, so declining to push it is observable). Reordering inside one test
   does not work here: the field is boolean, so whichever half runs second
   finds spec and AWS in agreement and asserts nothing. The edit case also
   earns its own test because enableDNSSupport is pushed by
   ModifyVpcAttribute in the custom update path, not by tags.Sync.

3. `except: pass` around `assert deleted` in the fixture teardown hid leaked
   VPCs and CRs, and swallowed KeyboardInterrupt/SystemExit. Removed, so a
   failed delete fails the teardown. The budget goes from 30s to 60s at the
   same time: a VPC CR queued behind a busy controller regularly needs more
   than 30s to clear its finalizer, and ec2's own test_vpc.py has flaked on
   that exact assertion, so asserting on a 30s budget would trade a silent
   leak for a flake.

   The out-of-band `external` tag cleanup also moved into a finally, since as
   a trailing statement it was skipped whenever an assertion above it failed.
Same two defects @gustavodiaz7722 found in elbv2-controller#90, which are
generic to ACK drift e2e tests rather than specific to that repo. Verified
present here against this controller's own config.

1. The drift-survives and stays-Synced assertions could not fail.

An out-of-band AWS change produces no watch event, and this controller's resync
period is the runtime default of 10 hours: config/controller/deployment.yaml
(what controller-setup.sh deploys via kustomize) passes no
--reconcile-default-resync-seconds or --reconcile-resource-resync-seconds, and
pkg/resource/vpc/manager_factory.go returns 0 from RequeueOnSuccessSeconds(), so
getResyncPeriod falls through to defaultResyncPeriod. No reconcile occurred
during MODIFY_WAIT_AFTER_SECONDS, so asserting the external change survived
observed that nobody had looked -- it held identically with the feature removed.
The Synced checks were worse: wait_on_condition returned on its first poll off
the ACK.ResourceSynced condition written back at create.

Note this is a different vacuity from the one 5b177cf addressed. That was spec
and AWS agreeing so no delta existed to suppress; this is no reconcile happening
at all. Fixing the first did not touch the second.

Affected: test_tags_drift_ignored, test_scalar_external_drift_ignored and
test_declared_tag_value_drift_ignored, all of which drift out-of-band via
CreateTags / ModifyVpcAttribute.

Each now captures condition.get_synced_last_transition_time before the drift,
forces a reconcile, and requires via wait_on_condition_after that a reconcile
which started after the drift completed with ACK.ResourceSynced=True. That gate
carries both original claims, so the separate (vacuous) Synced waits are gone
rather than kept. A precondition assert also confirms AWS reports the drift
before the reconcile is forced, so a race with the out-of-band call cannot make
a test pass for an unrelated reason.

The reconcile is forced by patching an inert annotation rather than a spec field,
because the runtime adds AnnotationChangedPredicate to the event filter whenever
the IgnoreFieldDrift gate is on (reconciler.go SetupWithManager; the default is
GenerationChangedPredicate alone). Keeping the probe off the spec means the only
delta the reconcile sees is the external drift itself. The probe value is a
timestamp, not a constant, so the second call within test_tags_drift_ignored
really changes the annotation -- re-patching an identical value would fire no
event and silently degrade the wait to a no-op.

test_scalar_spec_edit_not_pushed and the spec-edit half of
test_tags_drift_ignored get the gate too, via _await_reconcile_after. Those
patches do bump metadata.generation so a reconcile is queued, but a fixed sleep
never established that it had finished.

MODIFY_WAIT_AFTER_SECONDS is now unused and removed.

2. The fixture read status.vpcID before it existed.

wait_resource_consumed_by_controller returns as soon as the resource has any
.status, which is the first status write and can predate status.vpcID. Every
test reads vpcID out of the CR ignore_field_drift_vpc yields, so that snapshot
could hand them a KeyError they cannot recover from. The fixture now waits for
ACK.ResourceSynced, then re-reads and asserts vpcID is present before yielding.

Signed-off-by: Hao Wang <rhaowang@amazon.com>
@sapphirew
sapphirew force-pushed the test/vpc-ignore-field-drift-e2e branch from e7ca069 to bdbb4b0 Compare September 4, 2026 21:49
@ack-prow ack-prow Bot removed the lgtm Indicates that a PR is ready to be merged. label Sep 4, 2026
@ack-prow

ack-prow Bot commented Sep 4, 2026

Copy link
Copy Markdown

New changes are detected. LGTM label has been removed.

@ack-prow

ack-prow Bot commented Sep 5, 2026

Copy link
Copy Markdown

@sapphirew: 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-kind-e2e bdbb4b0 link true /test ec2-kind-e2e

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants