Commit 85b7ce9
authored
feat(cluster): Add controlPlaneEgressMode support (#249)
Description of changes:
EKS launched [controlPlaneEgressMode](https://aws.amazon.com/about-aws/whats-new/2026/06/amazon-eks-customer-routed-control-plane-egress), which lets customers route control plane egress traffic through their own VPC (`CUSTOMER_ROUTED`) instead of AWS managed networking (`AWS_MANAGED`). This adds support for configuring and updating it on the `Cluster` resource.
Supersedes #228, which I've taken over. The `updateControlPlaneEgressMode` handler is Cavaughn's work and he's credited as co-author; the rest has been rebased onto `main` and reworked. #228 can be closed once this merges.
### Why the field was ignored, and what unblocks it
The SDK bump to v1.91.0 (#245) deliberately added three `ignore.field_paths` entries for this field, with the note *"ignore it until the controller reconciles it as its own update type"*. The reason: EKS accepts only one type of update per `UpdateClusterConfig` call, and because `newVpcConfigRequest` populates every VPC field, the egress mode was riding along on endpoint-access and subnet updates and EKS rejected those with `Only one type of update can be allowed`.
This PR satisfies that precondition, so the ignore entries are removed and the field is generated normally. Each VPC helper now sends only its own fields.
### Commits
1. **Generate the field** — remove the three ignore entries, add `late_initialize`, regenerate. EKS defaults the mode to `AWS_MANAGED` and returns it on read even when the spec omits it, so without `late_initialize` the controller treats the returned default as drift against a nil spec value and never converges. Regenerated with code-generator `v0.63.0` against `aws_service_sdk_version` `v1.91.0`.
2. **Reconcile it as its own update type** — a dedicated `customUpdate` branch and handler. The endpoint-access and subnet/security-group helpers clear the egress mode; the new handler clears everything else. The desired value is nil-guarded before dereference (clearing the field previously panicked). Whether a transition is legal is EKS's rule, not the controller's, so the request is sent as-is and any refusal surfaces as the service's own error.
3. **e2e coverage** — one test for reconcile behaviour, one for the create path.
### A note on the unsupported transition
EKS refuses to move a cluster from `CUSTOMER_ROUTED` back to `AWS_MANAGED`:
```
InvalidParameterException: Update of ControlPlaneEgressMode from CUSTOMER_ROUTED to AWS_MANAGED is not supported
```
That code isn't in the resource's `terminal_codes`, so it's reported as recoverable and retried indefinitely. I deliberately did **not** encode the transition rule in the controller — that would break the moment AWS relaxes it, and it silently skips a call the service is the authority on. Making this terminal is a `terminal_codes` decision and is left out of this PR. Worth noting `InvalidParameterException` is broad on this resource (a rejected `publicAccessCidrs` value returns it too, and also retries forever today), so it may be worth adding regardless — happy to do it here if reviewers prefer.
### Testing
- `go build ./...` and `go test ./...` pass; `gofmt` clean.
- Generated output is reproducible: `ack-workspace build eks` (code-generator `v0.63.0`) produces the committed files, including the `delta.go` comparison and the `!= ""` guard on the read path.
This exact commit was deployed to a live EKS cluster and each behaviour exercised end to end:
| Behaviour | Result |
|---|---|
| Create with the field omitted | `late_initialize` adopts `AWS_MANAGED`; Synced with no drift and no API calls |
| Clear the field from the spec | No panic, no terminal condition; late-init re-adopts the observed value |
| Endpoint-access-only update, egress mode set | `EndpointAccessUpdate` succeeds; params carry only endpoint-access fields, no `ControlPlaneEgressMode`. No `Only one type of update can be allowed` rejection |
| `AWS_MANAGED` -> `CUSTOMER_ROUTED` | `ControlPlaneEgressUpdate` succeeds carrying only `ControlPlaneEgressMode`; endpoint access untouched |
| `CUSTOMER_ROUTED` -> `AWS_MANAGED` | Refused by EKS; surfaces as `ACK.Recoverable` with the message above, cluster left unchanged, no update record created |
| Create directly with `CUSTOMER_ROUTED` | Cluster reaches ACTIVE in that mode, Synced, and `list_updates` is empty — the value was carried by `CreateCluster`, not a follow-up update |
One gap reviewers should know about: **the e2e tests have not been run under pytest.** They are verified by syntax and structure only, and the behaviours they assert were validated by driving the same sequences against a live cluster by hand. I did not have a bootstrapped e2e environment.
The create-path check used public subnets from a dev-account VPC rather than the e2e bootstrap VPC, so subnet IDs differ, but the topology is the same (public subnets with an internet gateway route) and EKS accepted `CUSTOMER_ROUTED` at create there.
Also note `test/e2e/requirements.txt` pins only `acktest`, and the botocore it resolves does not yet model `controlPlaneEgressMode` — botocore silently drops response members it doesn't know, so AWS-side assertions on it go through a helper that skips just that comparison rather than failing for an unrelated reason. A botocore floor in `requirements.txt` would let CI make the stronger assertion. All AWS-side values quoted above were read with an SDK build that does model the field.
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.1 parent f450a4e commit 85b7ce9
14 files changed
Lines changed: 329 additions & 28 deletions
File tree
- apis/v1alpha1
- config/crd/bases
- helm/crds
- pkg
- resource/cluster
- version
- test/e2e
- resources
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
| 2 | + | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | | - | |
| 6 | + | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
| 10 | + | |
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
199 | 199 | | |
200 | 200 | | |
201 | 201 | | |
| 202 | + | |
| 203 | + | |
202 | 204 | | |
203 | 205 | | |
204 | 206 | | |
| |||
690 | 692 | | |
691 | 693 | | |
692 | 694 | | |
693 | | - | |
694 | | - | |
695 | | - | |
696 | | - | |
697 | | - | |
698 | | - | |
699 | | - | |
700 | | - | |
701 | | - | |
702 | 695 | | |
703 | 696 | | |
704 | 697 | | |
| |||
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
383 | 383 | | |
384 | 384 | | |
385 | 385 | | |
| 386 | + | |
| 387 | + | |
386 | 388 | | |
387 | 389 | | |
388 | 390 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
199 | 199 | | |
200 | 200 | | |
201 | 201 | | |
| 202 | + | |
| 203 | + | |
202 | 204 | | |
203 | 205 | | |
204 | 206 | | |
| |||
690 | 692 | | |
691 | 693 | | |
692 | 694 | | |
693 | | - | |
694 | | - | |
695 | | - | |
696 | | - | |
697 | | - | |
698 | | - | |
699 | | - | |
700 | | - | |
701 | | - | |
702 | 695 | | |
703 | 696 | | |
704 | 697 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
383 | 383 | | |
384 | 384 | | |
385 | 385 | | |
| 386 | + | |
| 387 | + | |
386 | 388 | | |
387 | 389 | | |
388 | 390 | | |
| |||
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
300 | 300 | | |
301 | 301 | | |
302 | 302 | | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
303 | 316 | | |
304 | 317 | | |
305 | 318 | | |
| |||
611 | 624 | | |
612 | 625 | | |
613 | 626 | | |
| 627 | + | |
614 | 628 | | |
615 | 629 | | |
616 | 630 | | |
| |||
637 | 651 | | |
638 | 652 | | |
639 | 653 | | |
| 654 | + | |
640 | 655 | | |
641 | 656 | | |
642 | 657 | | |
| |||
904 | 919 | | |
905 | 920 | | |
906 | 921 | | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
| 936 | + | |
| 937 | + | |
| 938 | + | |
| 939 | + | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
| 947 | + | |
| 948 | + | |
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
0 commit comments