Skip to content

feat: Add GlobalReplicationGroup resource support - #228

Open
NicholasBlaskey wants to merge 1 commit into
aws-controllers-k8s:mainfrom
NicholasBlaskey:feat/global-replication-group
Open

NicholasBlaskey wants to merge 1 commit into
aws-controllers-k8s:mainfrom
NicholasBlaskey:feat/global-replication-group

Conversation

@NicholasBlaskey

@NicholasBlaskey NicholasBlaskey commented Aug 12, 2026

Copy link
Copy Markdown

Add support for the ElastiCache GlobalReplicationGroup resource, which backs the Global Datastore feature for cross-region replication.

Implementation notes:

  • generator.yaml: resource config, field renames, custom delete/update hooks, and a NodeGroupCount custom field for declarative shard scaling
  • hooks.go: multi-phase delete (disassociate secondaries before deleting the global group), custom update (shard scaling plus modify), and Describe output mapping
  • E2E tests covering create, modify, shard scale up/down, and invalid-primary terminal handling

Generated with SERVICE=elasticache AWS_SDK_GO_VERSION=v1.41.0 make build-controller from code-generator v0.62.0 (build hash db232581), matching the version recorded in ack-generate-metadata.yaml.

Resolves aws-controllers-k8s/community#1048

Issue #, if available:

Description of changes:

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 added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 12, 2026
@ack-prow

ack-prow Bot commented Aug 12, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: NicholasBlaskey
Once this PR has been reviewed and has the lgtm label, please assign knottnt 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 jlbutler and sapphirew August 12, 2026 22:37
@ack-prow ack-prow Bot added the needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. label Aug 12, 2026
@ack-prow

ack-prow Bot commented Aug 12, 2026

Copy link
Copy Markdown

Hi @NicholasBlaskey. Thanks for your PR.

I'm waiting for a aws-controllers-k8s member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work.

Regular contributors should join the org to skip this step.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

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.

# 3->2 decrease against live AWS, so this budget is deliberately generous --
# the wait returns as soon as the target state is reached, so a large ceiling
# costs nothing on the happy path and only prevents flakes.
SCALE_WAIT_PERIODS = 80

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

this test takes around 70 minutes to run. Is that too long for ACK?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Does the bulk of that come from nodegroup scaling? You shouldn't need to wait for that entire operation in order to make sure that it works, I think confirming that the right scaling call is made once the GRG reaches the modifying state would be enough to ensure that the update for this case is working properly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yeah it is node group scaling.

This is a good idea, but unfortunately I think the API requires us to wait for the resource to finish scaling before deleting, so while this would speed up the test I think the overall run would take the same as it waits for the scale to finish in cleanup.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Then maybe it might be better to test the scaling logic in a unit test rather than an integration test. The other tests already cover the CRUD-reconcile-to-synced path, so we're not losing anything by testing the scaling logic with unit tests.

@NicholasBlaskey
NicholasBlaskey marked this pull request as ready for review August 13, 2026 17:47
@ack-prow ack-prow Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 13, 2026
@ack-prow
ack-prow Bot requested review from a-hilaly and michaelhtm August 13, 2026 17:47

@gobos12 gobos12 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice work @NicholasBlaskey! Left some comments below.

# 3->2 decrease against live AWS, so this budget is deliberately generous --
# the wait returns as soon as the target state is reached, so a large ceiling
# costs nothing on the happy path and only prevents flakes.
SCALE_WAIT_PERIODS = 80

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Does the bulk of that come from nodegroup scaling? You shouldn't need to wait for that entire operation in order to make sure that it works, I think confirming that the right scaling call is made once the GRG reaches the modifying state would be enough to ensure that the update for this case is working properly.

Comment thread generator.yaml Outdated
ModifyGlobalReplicationGroup:
custom_implementation: CustomModifyGlobalReplicationGroup
override_values:
ApplyImmediately: aws.Bool(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should this always default to true?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

default/override_values here I don't believe ends up doing anything here because of the custom implementation required.

Removed override_values block here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

To add some detail here, my understanding is we need a complete custom implementation for the whole method because certain fields on the global replication group cannot be sent in the same update (and others must be sent in the same update)

For example the API rejects sending Description, AutomaticFailoverEnabled, and CacheNodeType with anything else with

│ InvalidParameterValue: Global Replication Group modifications only support modifying one field per request.

// We can only proceed with delete logic when in a stable state:
// - "available" means secondaries are attached → need to disassociate first
// - "primary-only" means no secondaries → can delete directly
if !isGlobalPrimaryOnly(r) && !isGlobalAvailable(r) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm probably just confused on the wording in the comments, but wouldn't the delete get stuck here? It seems it's waiting for the resource to be in both the primary-only and available states, but that would require the resources to have secondaries attached while also not having any secondaries? Some clarification in the comments (whether that be here or in generator.yaml) would be nice.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

updated comment to be clearer

name: $RG_ID
spec:
cacheNodeType: cache.t3.micro
cacheNodeType: cache.r6g.large

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Does changing this affect the e2e tests for replication groups?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes. I don't think it caused it to fail but split this up to avoid impacting and causing the existing tests to use larger instance types than needed.

Comment thread generator.yaml Outdated
Comment on lines +503 to +514
GlobalReplicationGroupID:
is_read_only: true
ARN:
is_read_only: true
ClusterEnabled:
is_read_only: true
AtRestEncryptionEnabled:
is_read_only: true
TransitEncryptionEnabled:
is_read_only: true
AuthTokenEnabled:
is_read_only: true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are these "read-only" specs necessary? They seem to be a part of the data type and aren't required for any of the API calls, adding them here doesn't make sense to me.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

These shouldn't have been added. Removing them.

NicholasBlaskey added a commit to NicholasBlaskey/elasticache-controller that referenced this pull request Aug 17, 2026
Add support for the ElastiCache GlobalReplicationGroup resource, which
backs the Global Datastore feature for cross-region replication.

Implementation notes:
- generator.yaml: resource config, field renames, custom delete/update
  hooks, and a NodeGroupCount custom field for declarative shard scaling
- hooks.go: multi-phase delete (disassociate secondaries before deleting
  the global group), custom update (shard scaling plus modify), and
  Describe output mapping
- E2E tests covering create, modify, shard scale up/down, and
  invalid-primary terminal handling

Behaviors worth noting, all verified against live AWS:
- A global group with no secondaries reports 'primary-only' and never
  'available', so readiness cannot key off 'available' alone.
- Description must be renamed in the DescribeGlobalReplicationGroups
  output as well as the Create/Modify inputs. Without the Describe
  rename the field is never mapped back from AWS, so the observed state
  keeps the desired value and the delta never detects a change.
- AutomaticFailoverEnabled and CacheParameterGroupName are not returned
  by Describe at all. Their last-requested values are tracked in
  annotations, matching the existing ReplicationGroup resource, so the
  delta compares against what was actually last sent to AWS.
- A decrease in shard count requires GlobalNodeGroupsToRetain, which is
  why increase and decrease are separate API calls.
- Node group scaling holds the group in 'modifying' for 15-30 minutes,
  and on a decrease the shard count only drops at the very end, so the
  E2E waits key off shard count plus a settled status rather than status
  alone.
- Global Datastore requires large-and-above M/R/C-family node types and
  automatic failover enabled on the primary, so the GRG E2E tests use a
  dedicated ReplicationGroup fixture rather than the shared one used by
  test_replicationgroup.py.

Incorporates review feedback (see PR aws-controllers-k8s#228):
- Removed dead override_values/custom_implementation config for
  ModifyGlobalReplicationGroup and the Increase/DecreaseNodeGroups
  operations -- Update bypasses the generated request path entirely via
  update_operation.custom_method_name, so this config had no effect.
- Removed redundant is_read_only: true on six Status-only fields that
  the generator already places in Status by default.
- Clarified the delete-phase stable-state comment in hooks.go.

Generated with `SERVICE=elasticache AWS_SDK_GO_VERSION=v1.41.0 make
build-controller` from code-generator v0.62.1 (build hash 65d45b2e),
matching the version recorded in ack-generate-metadata.yaml.

Resolves aws-controllers-k8s/community#1048
@NicholasBlaskey
NicholasBlaskey force-pushed the feat/global-replication-group branch from b1fed40 to 5f5a582 Compare August 17, 2026 20:12
NicholasBlaskey added a commit to NicholasBlaskey/elasticache-controller that referenced this pull request Aug 18, 2026
Add support for the ElastiCache GlobalReplicationGroup resource, which
backs the Global Datastore feature for cross-region replication.

Implementation notes:
- generator.yaml: resource config, field renames, custom delete/update
  hooks, and a NodeGroupCount custom field for declarative shard scaling
- hooks.go: multi-phase delete (disassociate secondaries before deleting
  the global group), custom update (shard scaling plus modify), and
  Describe output mapping
- E2E tests covering create, modify, multi-field convergence, shard
  scale up/down, and invalid-primary terminal handling

Behaviors worth noting, all verified against live AWS:
- A global group with no secondaries reports 'primary-only' and never
  'available', so readiness cannot key off 'available' alone.
- Description must be renamed in the DescribeGlobalReplicationGroups
  output as well as the Create/Modify inputs. Without the Describe
  rename the field is never mapped back from AWS, so the observed state
  keeps the desired value and the delta never detects a change.
- AutomaticFailoverEnabled and CacheParameterGroupName are not returned
  by Describe at all. Their last-requested values are tracked in
  annotations, matching the existing ReplicationGroup resource, so the
  delta compares against what was actually last sent to AWS.
- A decrease in shard count requires GlobalNodeGroupsToRetain, which is
  why increase and decrease are separate API calls.
- Node group scaling holds the group in 'modifying' for 15-30 minutes,
  and on a decrease the shard count only drops at the very end, so the
  E2E waits key off shard count plus a settled status rather than status
  alone.
- Global Datastore requires large-and-above M/R/C-family node types and
  automatic failover enabled on the primary, so the GRG E2E tests use a
  dedicated ReplicationGroup fixture rather than the shared one used by
  test_replicationgroup.py.

Update is handled entirely by a custom hook
(update_operation.custom_method_name) rather than the generated request
path, because ModifyGlobalReplicationGroup does not accept arbitrary
combinations of fields in one request. Verified empirically against the
live API:
- The independent fields (Description, AutomaticFailoverEnabled,
  CacheNodeType) must each be sent in their own Modify call; a request
  carrying more than one is rejected.
- The engine-upgrade fields must be sent together: Engine + EngineVersion
  for a minor upgrade, and Engine + EngineVersion + CacheParameterGroupName
  for a major upgrade (a lone Engine yields "No modifications requested";
  Engine + EngineVersion without a parameter group yields "Parameter group
  must be specified for major engine version upgrade").
The hook therefore applies one logical change per reconcile, comparing
desired against observed state per field and letting any remaining changes
converge over subsequent reconciles. Engine versions are compared with
util.EngineVersionsMatch so an AWS-normalized "7.1.0" is not seen as
different from a requested "7.1" (which would otherwise re-issue the
upgrade every reconcile). The generated request builder cannot express
these rules -- it only checks whether a Spec field is non-nil, not whether
it changed, and reflected-back fields are non-nil on every reconcile after
the first -- so a fully custom Update handler is required here even though
the sibling ReplicationGroup resource can rely on the generated path (its
ModifyReplicationGroup API has no such field-combination restriction).

Incorporates review feedback (see PR aws-controllers-k8s#228):
- Removed dead override_values config for ModifyGlobalReplicationGroup and
  the Increase/DecreaseNodeGroups operations -- Update bypasses the
  generated request path entirely via update_operation.custom_method_name,
  so this config had no effect.
- Removed redundant is_read_only: true on six Status-only fields that
  the generator already places in Status by default.
- Clarified the delete-phase stable-state comment in hooks.go.

Generated with `SERVICE=elasticache AWS_SDK_GO_VERSION=v1.41.0 make
build-controller` from code-generator v0.62.1, matching the version
recorded in ack-generate-metadata.yaml.

Resolves aws-controllers-k8s/community#1048
@NicholasBlaskey
NicholasBlaskey force-pushed the feat/global-replication-group branch from 5f5a582 to 70c7b78 Compare August 18, 2026 16:48
@gobos12

gobos12 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

/ok-to-test

@ack-prow ack-prow Bot added ok-to-test Indicates a non-member PR verified by an org member that is safe to test. and removed needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. labels Aug 18, 2026
NicholasBlaskey added a commit to NicholasBlaskey/elasticache-controller that referenced this pull request Aug 18, 2026
Add support for the ElastiCache GlobalReplicationGroup resource, which
backs the Global Datastore feature for cross-region replication.

Implementation notes:
- generator.yaml: resource config, field renames, custom delete/update
  hooks, and a NodeGroupCount custom field for declarative shard scaling
- hooks.go: multi-phase delete (disassociate secondaries before deleting
  the global group), custom update (shard scaling plus modify), and
  Describe output mapping
- E2E tests covering create, modify, multi-field convergence, shard
  scale up/down, and invalid-primary terminal handling

Behaviors worth noting, all verified against live AWS:
- A global group with no secondaries reports 'primary-only' and never
  'available', so readiness cannot key off 'available' alone.
- Description must be renamed in the DescribeGlobalReplicationGroups
  output as well as the Create/Modify inputs. Without the Describe
  rename the field is never mapped back from AWS, so the observed state
  keeps the desired value and the delta never detects a change.
- AutomaticFailoverEnabled and CacheParameterGroupName are not returned
  by Describe at all. Their last-requested values are tracked in
  annotations, matching the existing ReplicationGroup resource, so the
  delta compares against what was actually last sent to AWS.
- A decrease in shard count requires GlobalNodeGroupsToRetain, which is
  why increase and decrease are separate API calls.
- Node group scaling holds the group in 'modifying' for 15-30 minutes,
  and on a decrease the shard count only drops at the very end, so the
  E2E waits key off shard count plus a settled status rather than status
  alone.
- Global Datastore requires large-and-above M/R/C-family node types and
  automatic failover enabled on the primary, so the GRG E2E tests use a
  dedicated ReplicationGroup fixture rather than the shared one used by
  test_replicationgroup.py.

Update is handled entirely by a custom hook
(update_operation.custom_method_name) rather than the generated request
path, because ModifyGlobalReplicationGroup does not accept arbitrary
combinations of fields in one request. Verified empirically against the
live API:
- The independent fields (Description, AutomaticFailoverEnabled,
  CacheNodeType) must each be sent in their own Modify call; a request
  carrying more than one is rejected.
- The engine-upgrade fields must be sent together: Engine + EngineVersion
  for a minor upgrade, and Engine + EngineVersion + CacheParameterGroupName
  for a major upgrade (a lone Engine yields "No modifications requested";
  Engine + EngineVersion without a parameter group yields "Parameter group
  must be specified for major engine version upgrade").
The hook therefore applies one logical change per reconcile, comparing
desired against observed state per field and letting any remaining changes
converge over subsequent reconciles. Engine versions are compared with
util.EngineVersionsMatch so an AWS-normalized "7.1.0" is not seen as
different from a requested "7.1" (which would otherwise re-issue the
upgrade every reconcile). The generated request builder cannot express
these rules -- it only checks whether a Spec field is non-nil, not whether
it changed, and reflected-back fields are non-nil on every reconcile after
the first -- so a fully custom Update handler is required here even though
the sibling ReplicationGroup resource can rely on the generated path (its
ModifyReplicationGroup API has no such field-combination restriction).

Incorporates review feedback (see PR aws-controllers-k8s#228):
- Removed dead override_values config for ModifyGlobalReplicationGroup and
  the Increase/DecreaseNodeGroups operations -- Update bypasses the
  generated request path entirely via update_operation.custom_method_name,
  so this config had no effect.
- Removed redundant is_read_only: true on six Status-only fields that
  the generator already places in Status by default.
- Clarified the delete-phase stable-state comment in hooks.go.

Generated with `SERVICE=elasticache AWS_SDK_GO_VERSION=v1.41.0 make
build-controller` from code-generator v0.62.1, matching the version
recorded in ack-generate-metadata.yaml.

Resolves aws-controllers-k8s/community#1048
@NicholasBlaskey
NicholasBlaskey force-pushed the feat/global-replication-group branch from 70c7b78 to 29bdf9f Compare August 18, 2026 17:45
NicholasBlaskey added a commit to NicholasBlaskey/elasticache-controller that referenced this pull request Aug 18, 2026
Add support for the ElastiCache GlobalReplicationGroup resource, which
backs the Global Datastore feature for cross-region replication.

Implementation notes:
- generator.yaml: resource config, field renames, custom delete/update
  hooks, and a NodeGroupCount custom field for declarative shard scaling
- hooks.go: multi-phase delete (disassociate secondaries before deleting
  the global group), custom update (shard scaling plus modify), and
  Describe output mapping
- E2E tests covering create, modify, multi-field convergence, shard
  scale up/down, and invalid-primary terminal handling

Behaviors worth noting, all verified against live AWS:
- A global group with no secondaries reports 'primary-only' and never
  'available', so readiness cannot key off 'available' alone.
- Description must be renamed in the DescribeGlobalReplicationGroups
  output as well as the Create/Modify inputs. Without the Describe
  rename the field is never mapped back from AWS, so the observed state
  keeps the desired value and the delta never detects a change.
- AutomaticFailoverEnabled and CacheParameterGroupName are not returned
  by Describe at all. Their last-requested values are tracked in
  annotations, matching the existing ReplicationGroup resource, so the
  delta compares against what was actually last sent to AWS.
- A decrease in shard count requires GlobalNodeGroupsToRetain, which is
  why increase and decrease are separate API calls.
- Node group scaling holds the group in 'modifying' for 15-30 minutes,
  and on a decrease the shard count only drops at the very end, so the
  E2E waits key off shard count plus a settled status rather than status
  alone.
- Global Datastore requires large-and-above M/R/C-family node types and
  automatic failover enabled on the primary, so the GRG E2E tests use a
  dedicated ReplicationGroup fixture rather than the shared one used by
  test_replicationgroup.py.

Update is handled entirely by a custom hook
(update_operation.custom_method_name) rather than the generated request
path, because ModifyGlobalReplicationGroup does not accept arbitrary
combinations of fields in one request. Verified empirically against the
live API:
- The independent fields (Description, AutomaticFailoverEnabled,
  CacheNodeType) must each be sent in their own Modify call; a request
  carrying more than one is rejected.
- The engine-upgrade fields must be sent together: Engine + EngineVersion
  for a minor upgrade, and Engine + EngineVersion + CacheParameterGroupName
  for a major upgrade (a lone Engine yields "No modifications requested";
  Engine + EngineVersion without a parameter group yields "Parameter group
  must be specified for major engine version upgrade").
The hook therefore applies one logical change per reconcile, comparing
desired against observed state per field and letting any remaining changes
converge over subsequent reconciles. Engine versions are compared with
util.EngineVersionsMatch so an AWS-normalized "7.1.0" is not seen as
different from a requested "7.1" (which would otherwise re-issue the
upgrade every reconcile). The generated request builder cannot express
these rules -- it only checks whether a Spec field is non-nil, not whether
it changed, and reflected-back fields are non-nil on every reconcile after
the first -- so a fully custom Update handler is required here even though
the sibling ReplicationGroup resource can rely on the generated path (its
ModifyReplicationGroup API has no such field-combination restriction).

Incorporates review feedback (see PR aws-controllers-k8s#228):
- Removed dead override_values config for ModifyGlobalReplicationGroup and
  the Increase/DecreaseNodeGroups operations -- Update bypasses the
  generated request path entirely via update_operation.custom_method_name,
  so this config had no effect.
- Removed redundant is_read_only: true on six Status-only fields that
  the generator already places in Status by default.
- Clarified the delete-phase stable-state comment in hooks.go.

Generated with `SERVICE=elasticache AWS_SDK_GO_VERSION=v1.41.0 make
build-controller` from code-generator v0.62.1-4-gd4aaca5 (main), matching
the version recorded in ack-generate-metadata.yaml.

Resolves aws-controllers-k8s/community#1048
@NicholasBlaskey
NicholasBlaskey force-pushed the feat/global-replication-group branch from 29bdf9f to a9ad254 Compare August 18, 2026 18:48
NicholasBlaskey added a commit to NicholasBlaskey/elasticache-controller that referenced this pull request Aug 19, 2026
Add support for the ElastiCache GlobalReplicationGroup resource, which
backs the Global Datastore feature for cross-region replication.

Implementation notes:
- generator.yaml: resource config, field renames, custom delete/update
  hooks, and a NodeGroupCount custom field for declarative shard scaling
- hooks.go: multi-phase delete (disassociate secondaries before deleting
  the global group), custom update (shard scaling plus modify), and
  Describe output mapping
- E2E tests covering create, modify, no-op automatic-failover handling,
  multi-field convergence, shard scale up/down, and invalid-primary
  terminal handling

Behaviors worth noting, all verified against live AWS:
- A global group with no secondaries reports 'primary-only' and never
  'available', so readiness cannot key off 'available' alone.
- Description must be renamed in the DescribeGlobalReplicationGroups
  output as well as the Create/Modify inputs. Without the Describe
  rename the field is never mapped back from AWS, so the observed state
  keeps the desired value and the delta never detects a change.
- AutomaticFailoverEnabled and CacheParameterGroupName are not returned
  as top-level fields by DescribeGlobalReplicationGroups. AutomaticFailover
  is derived from live per-member state (which Describe does return), so
  the delta compares against reality rather than a tracked value; a global
  datastore's primary always has failover enabled, so a redundant Modify
  (which AWS rejects with "Requested product type is the same current
  product type") is never issued. CacheParameterGroupName cannot be read
  back at all -- ElastiCache copies it per member -- so its last-requested
  value is tracked in an annotation, and it is only valid as part of a
  major engine upgrade (a change on its own is surfaced as terminal).
- A decrease in shard count requires GlobalNodeGroupsToRetain, which is
  why increase and decrease are separate API calls.
- Node group scaling holds the group in 'modifying' for 15-30 minutes,
  and on a decrease the shard count only drops at the very end, so the
  E2E waits key off shard count plus a settled status rather than status
  alone.
- Global Datastore requires large-and-above M/R/C-family node types and
  automatic failover enabled on the primary, so the GRG E2E tests use a
  dedicated ReplicationGroup fixture rather than the shared one used by
  test_replicationgroup.py.

Update is handled entirely by a custom hook
(update_operation.custom_method_name) rather than the generated request
path, because ModifyGlobalReplicationGroup does not accept arbitrary
combinations of fields in one request. Verified empirically against the
live API:
- The independent fields (Description, AutomaticFailoverEnabled,
  CacheNodeType) must each be sent in their own Modify call; a request
  carrying more than one is rejected.
- The engine-upgrade fields must be sent together: Engine + EngineVersion
  for a minor upgrade, and Engine + EngineVersion + CacheParameterGroupName
  for a major upgrade (a lone Engine yields "No modifications requested";
  Engine + EngineVersion without a parameter group yields "Parameter group
  must be specified for major engine version upgrade").
The hook therefore applies one logical change per reconcile. When more
than one field changed it applies the first and returns an explicit
requeue for the rest, because IsSynced() is status-based: a fast change
(e.g. Description) returns the group to a synced status almost immediately,
so the remaining delta would otherwise be stranded until the next resync.
The delta is recomputed from the CR spec and live AWS state each pass, so
this converges over multiple reconciles and is correct across controller
restarts (no in-memory pending-change state). Engine versions are compared
with util.EngineVersionsMatch so an AWS-normalized "7.1.0" is not seen as
different from a requested "7.1" (which would otherwise re-issue the
upgrade every reconcile). Permanent misconfigurations (a major upgrade
with no parameter group, or a parameter-group change outside an upgrade)
are surfaced as terminal errors rather than requeued forever. The
generated request builder cannot express these rules -- it only checks
whether a Spec field is non-nil, not whether it changed -- so a fully
custom Update handler is required here even though the sibling
ReplicationGroup resource can rely on the generated path (its
ModifyReplicationGroup API has no such field-combination restriction).

Incorporates review feedback (see PR aws-controllers-k8s#228):
- Removed dead override_values config for ModifyGlobalReplicationGroup and
  the Increase/DecreaseNodeGroups operations -- Update bypasses the
  generated request path entirely via update_operation.custom_method_name,
  so this config had no effect.
- Removed redundant is_read_only: true on six Status-only fields that
  the generator already places in Status by default.
- Clarified the delete-phase stable-state comment in hooks.go.

Generated with `SERVICE=elasticache AWS_SDK_GO_VERSION=v1.41.0 make
build-controller` from code-generator v0.62.1-4-gd4aaca5 (main), matching
the version recorded in ack-generate-metadata.yaml.

Resolves aws-controllers-k8s/community#1048
@NicholasBlaskey
NicholasBlaskey force-pushed the feat/global-replication-group branch from a9ad254 to 998ede1 Compare August 19, 2026 05:15
NicholasBlaskey added a commit to NicholasBlaskey/elasticache-controller that referenced this pull request Aug 19, 2026
Add support for the ElastiCache GlobalReplicationGroup resource, which
backs the Global Datastore feature for cross-region replication.

Implementation notes:
- generator.yaml: resource config, field renames, custom delete/update
  hooks, and a NodeGroupCount custom field for declarative shard scaling
- hooks.go: multi-phase delete (disassociate secondaries before deleting
  the global group), custom update (shard scaling plus modify), and
  Describe output mapping
- E2E tests covering create, modify, no-op automatic-failover handling,
  multi-field convergence, shard scale up/down, and invalid-primary
  terminal handling

Behaviors worth noting, all verified against live AWS:
- A global group with no secondaries reports 'primary-only' and never
  'available', so readiness cannot key off 'available' alone.
- Description must be renamed in the DescribeGlobalReplicationGroups
  output as well as the Create/Modify inputs. Without the Describe
  rename the field is never mapped back from AWS, so the observed state
  keeps the desired value and the delta never detects a change.
- AutomaticFailoverEnabled and CacheParameterGroupName are not returned
  as top-level fields by DescribeGlobalReplicationGroups. AutomaticFailover
  is derived from live per-member state (which Describe does return), so
  the delta compares against reality rather than a tracked value; a global
  datastore's primary always has failover enabled, so a redundant Modify
  (which AWS rejects with "Requested product type is the same current
  product type") is never issued. CacheParameterGroupName cannot be read
  back at all -- ElastiCache copies it per member -- so its last-requested
  value is tracked in an annotation, and it is only valid as part of a
  major engine upgrade (a change on its own is surfaced as terminal).
- A decrease in shard count requires GlobalNodeGroupsToRetain, which is
  why increase and decrease are separate API calls.
- Node group scaling holds the group in 'modifying' for 15-30 minutes,
  and on a decrease the shard count only drops at the very end, so the
  E2E waits key off shard count plus a settled status rather than status
  alone.
- Global Datastore requires large-and-above M/R/C-family node types and
  automatic failover enabled on the primary, so the GRG E2E tests use a
  dedicated ReplicationGroup fixture rather than the shared one used by
  test_replicationgroup.py.

Update is handled entirely by a custom hook
(update_operation.custom_method_name) rather than the generated request
path, because ModifyGlobalReplicationGroup does not accept arbitrary
combinations of fields in one request. Verified empirically against the
live API:
- The independent fields (Description, AutomaticFailoverEnabled,
  CacheNodeType) must each be sent in their own Modify call; a request
  carrying more than one is rejected.
- The engine-upgrade fields must be sent together: Engine + EngineVersion
  for a minor upgrade, and Engine + EngineVersion + CacheParameterGroupName
  for a major upgrade (a lone Engine yields "No modifications requested";
  Engine + EngineVersion without a parameter group yields "Parameter group
  must be specified for major engine version upgrade").
The hook therefore applies one logical change per reconcile. When more
than one field changed it applies the first and returns an explicit
requeue for the rest, because IsSynced() is status-based: a fast change
(e.g. Description) returns the group to a synced status almost immediately,
so the remaining delta would otherwise be stranded until the next resync.
The delta is recomputed from the CR spec and live AWS state each pass, so
this converges over multiple reconciles and is correct across controller
restarts (no in-memory pending-change state). Engine versions are compared
with util.EngineVersionsMatch so an AWS-normalized "7.1.0" is not seen as
different from a requested "7.1" (which would otherwise re-issue the
upgrade every reconcile). Permanent misconfigurations (a major upgrade
with no parameter group, or a parameter-group change outside an upgrade)
are surfaced as terminal errors rather than requeued forever. The
generated request builder cannot express these rules -- it only checks
whether a Spec field is non-nil, not whether it changed -- so a fully
custom Update handler is required here even though the sibling
ReplicationGroup resource can rely on the generated path (its
ModifyReplicationGroup API has no such field-combination restriction).

Incorporates review feedback (see PR aws-controllers-k8s#228):
- Removed dead override_values config for ModifyGlobalReplicationGroup and
  the Increase/DecreaseNodeGroups operations -- Update bypasses the
  generated request path entirely via update_operation.custom_method_name,
  so this config had no effect.
- Removed redundant is_read_only: true on six Status-only fields that
  the generator already places in Status by default.
- Clarified the delete-phase stable-state comment in hooks.go.

Generated with `SERVICE=elasticache AWS_SDK_GO_VERSION=v1.41.10 make
build-controller` from code-generator v0.62.1-4-gd4aaca5 (main), matching
the version recorded in ack-generate-metadata.yaml.

Resolves aws-controllers-k8s/community#1048
@NicholasBlaskey
NicholasBlaskey force-pushed the feat/global-replication-group branch from 998ede1 to 6e044a8 Compare August 19, 2026 19:36
NicholasBlaskey added a commit to NicholasBlaskey/elasticache-controller that referenced this pull request Aug 19, 2026
Add support for the ElastiCache GlobalReplicationGroup resource, which
backs the Global Datastore feature for cross-region replication.

Implementation notes:
- generator.yaml: resource config, field renames, custom delete/update
  hooks, and a NodeGroupCount custom field for declarative shard scaling
- hooks.go: multi-phase delete (disassociate secondaries before deleting
  the global group), custom update (shard scaling plus modify), and
  Describe output mapping
- E2E tests covering create, modify, no-op automatic-failover handling,
  multi-field convergence, shard scale up/down, and invalid-primary
  terminal handling

Behaviors worth noting, all verified against live AWS:
- A global group with no secondaries reports 'primary-only' and never
  'available', so readiness cannot key off 'available' alone.
- Description must be renamed in the DescribeGlobalReplicationGroups
  output as well as the Create/Modify inputs. Without the Describe
  rename the field is never mapped back from AWS, so the observed state
  keeps the desired value and the delta never detects a change.
- AutomaticFailoverEnabled and CacheParameterGroupName are not returned
  as top-level fields by DescribeGlobalReplicationGroups. AutomaticFailover
  is derived from live per-member state (which Describe does return), so
  the delta compares against reality rather than a tracked value; a global
  datastore's primary always has failover enabled, so a redundant Modify
  (which AWS rejects with "Requested product type is the same current
  product type") is never issued. CacheParameterGroupName cannot be read
  back at all -- ElastiCache copies it per member -- so its last-requested
  value is tracked in an annotation, and it is only valid as part of a
  major engine upgrade (a change on its own is surfaced as terminal).
- A decrease in shard count requires GlobalNodeGroupsToRetain, which is
  why increase and decrease are separate API calls.
- Node group scaling holds the group in 'modifying' for 15-30 minutes,
  and on a decrease the shard count only drops at the very end, so the
  E2E waits key off shard count plus a settled status rather than status
  alone.
- Global Datastore requires large-and-above M/R/C-family node types and
  automatic failover enabled on the primary, so the GRG E2E tests use a
  dedicated ReplicationGroup fixture rather than the shared one used by
  test_replicationgroup.py.

Update is handled entirely by a custom hook
(update_operation.custom_method_name) rather than the generated request
path, because ModifyGlobalReplicationGroup does not accept arbitrary
combinations of fields in one request. Verified empirically against the
live API:
- The independent fields (Description, AutomaticFailoverEnabled,
  CacheNodeType) must each be sent in their own Modify call; a request
  carrying more than one is rejected.
- The engine-upgrade fields must be sent together: Engine + EngineVersion
  for a minor upgrade, and Engine + EngineVersion + CacheParameterGroupName
  for a major upgrade (a lone Engine yields "No modifications requested";
  Engine + EngineVersion without a parameter group yields "Parameter group
  must be specified for major engine version upgrade").
The hook therefore applies one logical change per reconcile. When more
than one field changed it applies the first and returns an explicit
requeue for the rest, because IsSynced() is status-based: a fast change
(e.g. Description) returns the group to a synced status almost immediately,
so the remaining delta would otherwise be stranded until the next resync.
The delta is recomputed from the CR spec and live AWS state each pass, so
this converges over multiple reconciles and is correct across controller
restarts (no in-memory pending-change state). Engine versions are compared
with util.EngineVersionsMatch so an AWS-normalized "7.1.0" is not seen as
different from a requested "7.1" (which would otherwise re-issue the
upgrade every reconcile). Permanent misconfigurations (a major upgrade
with no parameter group, or a parameter-group change outside an upgrade)
are surfaced as terminal errors rather than requeued forever. The
generated request builder cannot express these rules -- it only checks
whether a Spec field is non-nil, not whether it changed -- so a fully
custom Update handler is required here even though the sibling
ReplicationGroup resource can rely on the generated path (its
ModifyReplicationGroup API has no such field-combination restriction).

Incorporates review feedback (see PR aws-controllers-k8s#228):
- Removed dead override_values config for ModifyGlobalReplicationGroup and
  the Increase/DecreaseNodeGroups operations -- Update bypasses the
  generated request path entirely via update_operation.custom_method_name,
  so this config had no effect.
- Removed redundant is_read_only: true on six Status-only fields that
  the generator already places in Status by default.
- Clarified the delete-phase stable-state comment in hooks.go.

Generated with `SERVICE=elasticache AWS_SDK_GO_VERSION=v1.41.10 make
build-controller` from code-generator v0.62.1-4-gd4aaca5 (main), matching
the version recorded in ack-generate-metadata.yaml.

Resolves aws-controllers-k8s/community#1048
@NicholasBlaskey
NicholasBlaskey force-pushed the feat/global-replication-group branch from 6e044a8 to 6d904fc Compare August 19, 2026 19:55
NicholasBlaskey added a commit to NicholasBlaskey/elasticache-controller that referenced this pull request Aug 19, 2026
Add support for the ElastiCache GlobalReplicationGroup resource, which
backs the Global Datastore feature for cross-region replication.

Implementation notes:
- generator.yaml: resource config, field renames, custom delete/update
  hooks, and a NodeGroupCount custom field for declarative shard scaling
- hooks.go: multi-phase delete (disassociate secondaries before deleting
  the global group), custom update (shard scaling plus modify), and
  Describe output mapping
- E2E tests covering create, modify, no-op automatic-failover handling,
  multi-field convergence, shard scale up/down, and invalid-primary
  terminal handling

Behaviors worth noting, all verified against live AWS:
- A global group with no secondaries reports 'primary-only' and never
  'available', so readiness cannot key off 'available' alone.
- Description must be renamed in the DescribeGlobalReplicationGroups
  output as well as the Create/Modify inputs. Without the Describe
  rename the field is never mapped back from AWS, so the observed state
  keeps the desired value and the delta never detects a change.
- AutomaticFailoverEnabled and CacheParameterGroupName are not returned
  as top-level fields by DescribeGlobalReplicationGroups. AutomaticFailover
  is derived from live per-member state (which Describe does return), so
  the delta compares against reality rather than a tracked value; a global
  datastore's primary always has failover enabled, so a redundant Modify
  (which AWS rejects with "Requested product type is the same current
  product type") is never issued. CacheParameterGroupName cannot be read
  back at all -- ElastiCache copies it per member -- so its last-requested
  value is tracked in an annotation, and it is only valid as part of a
  major engine upgrade (a change on its own is surfaced as terminal).
- A decrease in shard count requires GlobalNodeGroupsToRetain, which is
  why increase and decrease are separate API calls.
- Node group scaling holds the group in 'modifying' for 15-30 minutes,
  and on a decrease the shard count only drops at the very end, so the
  E2E waits key off shard count plus a settled status rather than status
  alone.
- Global Datastore requires large-and-above M/R/C-family node types and
  automatic failover enabled on the primary, so the GRG E2E tests use a
  dedicated ReplicationGroup fixture rather than the shared one used by
  test_replicationgroup.py.

Update is handled entirely by a custom hook
(update_operation.custom_method_name) rather than the generated request
path, because ModifyGlobalReplicationGroup does not accept arbitrary
combinations of fields in one request. Verified empirically against the
live API:
- The independent fields (Description, AutomaticFailoverEnabled,
  CacheNodeType) must each be sent in their own Modify call; a request
  carrying more than one is rejected.
- The engine-upgrade fields must be sent together: Engine + EngineVersion
  for a minor upgrade, and Engine + EngineVersion + CacheParameterGroupName
  for a major upgrade (a lone Engine yields "No modifications requested";
  Engine + EngineVersion without a parameter group yields "Parameter group
  must be specified for major engine version upgrade").
The hook therefore applies one logical change per reconcile. When more
than one field changed it applies the first and returns an explicit
requeue for the rest, because IsSynced() is status-based: a fast change
(e.g. Description) returns the group to a synced status almost immediately,
so the remaining delta would otherwise be stranded until the next resync.
The delta is recomputed from the CR spec and live AWS state each pass, so
this converges over multiple reconciles and is correct across controller
restarts (no in-memory pending-change state). Engine versions are compared
with util.EngineVersionsMatch so an AWS-normalized "7.1.0" is not seen as
different from a requested "7.1" (which would otherwise re-issue the
upgrade every reconcile). Permanent misconfigurations (a major upgrade
with no parameter group, or a parameter-group change outside an upgrade)
are surfaced as terminal errors rather than requeued forever. The
generated request builder cannot express these rules -- it only checks
whether a Spec field is non-nil, not whether it changed -- so a fully
custom Update handler is required here even though the sibling
ReplicationGroup resource can rely on the generated path (its
ModifyReplicationGroup API has no such field-combination restriction).

Incorporates review feedback (see PR aws-controllers-k8s#228):
- Removed dead override_values config for ModifyGlobalReplicationGroup and
  the Increase/DecreaseNodeGroups operations -- Update bypasses the
  generated request path entirely via update_operation.custom_method_name,
  so this config had no effect.
- Removed redundant is_read_only: true on six Status-only fields that
  the generator already places in Status by default.
- Clarified the delete-phase stable-state comment in hooks.go.

Generated with `SERVICE=elasticache AWS_SDK_GO_VERSION=v1.41.10 make
build-controller` from code-generator v0.62.1-4-gd4aaca5 (main), matching
the version recorded in ack-generate-metadata.yaml.

Resolves aws-controllers-k8s/community#1048
@NicholasBlaskey
NicholasBlaskey force-pushed the feat/global-replication-group branch from 6d904fc to a27ad2d Compare August 19, 2026 21:21
NicholasBlaskey added a commit to NicholasBlaskey/elasticache-controller that referenced this pull request Aug 19, 2026
Add support for the ElastiCache GlobalReplicationGroup resource, which
backs the Global Datastore feature for cross-region replication.

Implementation notes:
- generator.yaml: resource config, field renames, custom delete/update
  hooks, and a NodeGroupCount custom field for declarative shard scaling
- hooks.go: multi-phase delete (disassociate secondaries before deleting
  the global group), custom update (shard scaling plus modify), and
  Describe output mapping
- E2E tests covering create, modify, no-op automatic-failover handling,
  multi-field convergence, shard scale up/down, and invalid-primary
  terminal handling

Behaviors worth noting, all verified against live AWS:
- A global group with no secondaries reports 'primary-only' and never
  'available', so readiness cannot key off 'available' alone.
- Description must be renamed in the DescribeGlobalReplicationGroups
  output as well as the Create/Modify inputs. Without the Describe
  rename the field is never mapped back from AWS, so the observed state
  keeps the desired value and the delta never detects a change.
- AutomaticFailoverEnabled and CacheParameterGroupName are not returned
  as top-level fields by DescribeGlobalReplicationGroups. AutomaticFailover
  is derived from live per-member state (which Describe does return), so
  the delta compares against reality rather than a tracked value; a global
  datastore's primary always has failover enabled, so a redundant Modify
  (which AWS rejects with "Requested product type is the same current
  product type") is never issued. CacheParameterGroupName cannot be read
  back at all -- ElastiCache copies it per member -- so its last-requested
  value is tracked in an annotation, and it is only valid as part of a
  major engine upgrade (a change on its own is surfaced as terminal).
- A decrease in shard count requires GlobalNodeGroupsToRetain, which is
  why increase and decrease are separate API calls.
- Node group scaling holds the group in 'modifying' for 15-30 minutes,
  and on a decrease the shard count only drops at the very end, so the
  E2E waits key off shard count plus a settled status rather than status
  alone.
- Global Datastore requires large-and-above M/R/C-family node types and
  automatic failover enabled on the primary, so the GRG E2E tests use a
  dedicated ReplicationGroup fixture rather than the shared one used by
  test_replicationgroup.py.

Update is handled entirely by a custom hook
(update_operation.custom_method_name) rather than the generated request
path, because ModifyGlobalReplicationGroup does not accept arbitrary
combinations of fields in one request. Verified empirically against the
live API:
- The independent fields (Description, AutomaticFailoverEnabled,
  CacheNodeType) must each be sent in their own Modify call; a request
  carrying more than one is rejected.
- The engine-upgrade fields must be sent together: Engine + EngineVersion
  for a minor upgrade, and Engine + EngineVersion + CacheParameterGroupName
  for a major upgrade (a lone Engine yields "No modifications requested";
  Engine + EngineVersion without a parameter group yields "Parameter group
  must be specified for major engine version upgrade").
The hook therefore applies one logical change per reconcile. When more
than one field changed it applies the first and returns an explicit
requeue for the rest, because IsSynced() is status-based: a fast change
(e.g. Description) returns the group to a synced status almost immediately,
so the remaining delta would otherwise be stranded until the next resync.
The delta is recomputed from the CR spec and live AWS state each pass, so
this converges over multiple reconciles and is correct across controller
restarts (no in-memory pending-change state). Engine versions are compared
with util.EngineVersionsMatch so an AWS-normalized "7.1.0" is not seen as
different from a requested "7.1" (which would otherwise re-issue the
upgrade every reconcile). Permanent misconfigurations (a major upgrade
with no parameter group, or a parameter-group change outside an upgrade)
are surfaced as terminal errors rather than requeued forever. The
generated request builder cannot express these rules -- it only checks
whether a Spec field is non-nil, not whether it changed -- so a fully
custom Update handler is required here even though the sibling
ReplicationGroup resource can rely on the generated path (its
ModifyReplicationGroup API has no such field-combination restriction).

Incorporates review feedback (see PR aws-controllers-k8s#228):
- Removed dead override_values config for ModifyGlobalReplicationGroup and
  the Increase/DecreaseNodeGroups operations -- Update bypasses the
  generated request path entirely via update_operation.custom_method_name,
  so this config had no effect.
- Removed redundant is_read_only: true on six Status-only fields that
  the generator already places in Status by default.
- Clarified the delete-phase stable-state comment in hooks.go.

Generated with `SERVICE=elasticache AWS_SDK_GO_VERSION=v1.41.10 make
build-controller` from code-generator v0.62.1-4-gd4aaca5 (main), matching
the version recorded in ack-generate-metadata.yaml.

Resolves aws-controllers-k8s/community#1048
@NicholasBlaskey
NicholasBlaskey force-pushed the feat/global-replication-group branch from a27ad2d to bb62e8b Compare August 19, 2026 21:44

@gustavodiaz7722 gustavodiaz7722 left a comment

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.

Reviewed this on top of bb62e8b, and validated the behavior on a live cluster (EKS + this controller image, 60s resync) against real ElastiCache in us-west-2, rather than by reading the diff alone. Build and unit tests are clean, the delete state machine works exactly as described (verified end-to-end, primary correctly retained), and the one-change-per-Modify strategy matches what the API actually accepts.

Five things need fixing, four of them one-liners in generator.yaml. The two substantive ones are that automaticFailoverEnabled and cacheParameterGroupName are currently discarded while the CR reports ACK.ResourceSynced=True. Details inline.

For reference, what I ran live: created a cache.r6g.large / redis 7.0 primary with failover enabled, then a GRG with cacheParameterGroupName set and no nodeGroupCount, waited for primary-only + Synced, then patched automaticFailoverEnabled: false, then patched description + cacheParameterGroupName together, then deleted. nodeGroupCount write-back and the scaling paths I did not exercise live (my primary was cluster-mode disabled, so GlobalNodeGroups came back empty).

Comment thread generator.yaml
override_values:
ApplyImmediately: aws.Bool(true)
DescribeGlobalReplicationGroups:
set_output_custom_method_name: CustomDescribeGlobalReplicationGroupsSetOutput

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.

automaticFailoverEnabled is silently dropped — the read path never asks for member info.

newListRequestPayload builds DescribeGlobalReplicationGroupsInput without ShowMemberInfo, and AWS only returns Members when that flag is true (// Returns the list of members that comprise the Global datastore.). So in the read path ko.Status.Members is always empty, deriveAutomaticFailoverEnabled always returns nil, and the fallback in CustomDescribeGlobalReplicationGroupsSetOutput sets latest.Spec.AutomaticFailoverEnabled = r.ko.Spec.AutomaticFailoverEnabled — i.e. observed is copied from desired, so the delta can never fire.

Observed live: patched spec.automaticFailoverEnabled: false on a synced GRG. Across 6 reconciles (60s resync) the CR stayed ACK.ResourceSynced=True, status.members stayed null, and AWS still reported the primary member as AutomaticFailover: enabled. describe-global-replication-groups --show-member-info returned the member the whole time.

Two consequences: the field is a no-op in both directions, and status.members is a CRD field that never populates.

This also means test_modify_automatic_failover_is_noop passes for a different reason than the one documented — no Modify is issued because there is no observed state at all, not because the derived value matched.

Suggested fix, declaratively here rather than in a hook (setSDKReadMany honors override_values):

  DescribeGlobalReplicationGroups:
    override_values:
      ShowMemberInfo: aws.Bool(true)
    set_output_custom_method_name: CustomDescribeGlobalReplicationGroupsSetOutput

Worth noting that once member info is actually present, customSetGlobalReplicationGroupOutput becomes a problem: it re-maps Members (which generated sdkFind already mapped) but always takes &afStr, whereas the generated code guards with if f10iter.AutomaticFailover != "". An empty enum then becomes a non-nil "", which deriveAutomaticFailoverEnabled sends to its default: branch and reports false instead of "undeterminable" — the opposite of the "nil status on a member -> nil" case the unit test asserts. Since the generated mapping already covers GlobalReplicationGroupId, GlobalNodeGroups, and Members, dropping that function looks like the cleanest fix.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

fixed

}
// Record the values of fields the Describe API does not return, so future
// reconciles can detect when the user actually changes them.
rm.setLastRequestedUnreadableFields(desired, ko)

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.

cacheParameterGroupName is recorded as "last requested" without ever being sent, so the field is silently discarded.

CreateGlobalReplicationGroupInput has exactly three members — GlobalReplicationGroupIdSuffix, PrimaryReplicationGroupId, GlobalReplicationGroupDescription. There is no parameter group on create, and the generated newCreateRequestPayload confirms it. But this line records spec.cacheParameterGroupName unconditionally, so restoreUnreadableFieldsFromAnnotations then hands back the user's own value as "observed" and the delta never fires. A cacheParameterGroupName set at creation time is therefore ignored, permanently, with no error and no condition.

The same thing happens on update. A CPGN change is not counted in pending (only descChanged, afeChanged, nodeTypeChanged, engineGroupChanged are), so when it accompanies another change the switch takes the other branch, the request goes out without CPGN, and setGlobalReplicationGroupOutput still records it as applied. The comment on modifyGlobalReplicationGroup — "the only annotated field is CacheParameterGroupName, which is only sent when it is the sole pending change" — holds for the requeue path but not for the annotation write.

Observed live: created with cacheParameterGroupName: default.redis7 (annotation written, nothing sent), then patched description + cacheParameterGroupName: default.redis7.cluster.on in one apply. The description landed on AWS, the annotation flipped to default.redis7.cluster.on, no ACK.Terminal, Synced=True — and AWS never received either value. default.redis7.cluster.on would have been rejected for that group had it actually been sent, which is what makes it a clean probe.

Suggested fix: only write the annotation where input.CacheParameterGroupName is actually populated (inside the engineGroupChanged branch, after the call succeeds), and drop the setLastRequestedUnreadableFields call from this create hook. Also fold the CPGN-differs condition into pending so a CPGN change combined with another change still reaches the existing terminal error instead of being swallowed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

fixed

Comment thread generator.yaml Outdated
- InvalidParameterCombination
- ServiceLinkedRoleNotFoundFault
- ReplicationGroupNotFoundFault
- InvalidReplicationGroupState

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.

InvalidReplicationGroupState is retryable, not terminal.

This is the error AWS returns while the primary is busy, and the message says so explicitly. Called directly against a primary that was not available:

An error occurred (InvalidReplicationGroupState) when calling the CreateGlobalReplicationGroup
operation: Replication Group is not in a valid state. Please retry after the modification on the
Replication group grg-review-primary is complete.

As a terminal code, applying a GRG manifest alongside a still-creating primary — the normal case when primaryReplicationGroupID is used rather than the ref, which is also what the e2e fixtures do — drives the resource permanently to ACK.Terminal=True and requires deleting and recreating the CR. Same for a GRG update issued while the primary happens to be modifying.

Suggested fix: drop this entry and let it requeue. ReplicationGroupNotFoundFault (line above) already covers the genuinely terminal case that test_invalid_primary_terminal exercises, so the test is unaffected.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

fixed

Comment thread generator.yaml Outdated
Comment on lines +517 to +520
Status:
is_read_only: true
print:
name: STATUS

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.

Duplicate STATUS printer column.

This field-level print: and the additional_columns STATUS entry below both emit .status.status, so the CRD carries the column twice:

  - additionalPrinterColumns:
    - jsonPath: .status.status
      name: STATUS
    - jsonPath: .status.status
      name: STATUS

Live kubectl get:

NAME         STATUS         STATUS         NODE-TYPE   SYNCED   AGE
grg-review   primary-only   primary-only               True     7m

Suggested fix: drop this print: block and keep the additional_columns entry, which carries the index that order_by: index uses.

Minor, in the same two lines: is_read_only: true is redundant here — Status appears only in Describe output, so the generator places it in Status by default. It is the same redundancy already removed from the other six fields in the last revision, so with the print: block gone the whole Status: entry can go.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

fixed

Comment thread generator.yaml Outdated
type: string
index: 10
- name: NODE-TYPE
json_path: .status.cacheNodeType

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.

NODE-TYPE column points at a path that is always empty.

CacheNodeType is generated into Spec, not Status — generated sdkFind does ko.Spec.CacheNodeType = elem.CacheNodeType (it is configured from: ModifyGlobalReplicationGroup). So .status.cacheNodeType never resolves and the column renders blank. Live, with spec.cacheNodeType: cache.r6g.large set:

NAME         STATUS         STATUS         NODE-TYPE   SYNCED   AGE
grg-review   primary-only   primary-only               True     7m

Suggested fix: json_path: .spec.cacheNodeType.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

fixed

@NicholasBlaskey
NicholasBlaskey force-pushed the feat/global-replication-group branch from bb62e8b to 9f22fb8 Compare August 28, 2026 23:33
@ack-prow

ack-prow Bot commented Aug 28, 2026

Copy link
Copy Markdown

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

Test name Commit Details Required Rerun command
elasticache-kind-e2e 9f22fb8 link true /test elasticache-kind-e2e
elasticache-crd-compat-check 9f22fb8 link true /test elasticache-crd-compat-check
elasticache-verify-attribution 9f22fb8 link false /test elasticache-verify-attribution
elasticache-recommended-policy-test 9f22fb8 link true /test elasticache-recommended-policy-test
elasticache-verify-code-gen 9f22fb8 link true /test elasticache-verify-code-gen
elasticache-unit-test 9f22fb8 link true /test elasticache-unit-test
elasticache-release-test 9f22fb8 link true /test elasticache-release-test
elasticache-metadata-file-test 9f22fb8 link true /test elasticache-metadata-file-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.

@gustavodiaz7722

Copy link
Copy Markdown
Member

@NicholasBlaskey Can you fix merge conflict? There was a new runtime release that was merged #232

Comment on lines +1 to +10
// After a successful Create call, capture the full GlobalReplicationGroupId
// (which includes the auto-generated region prefix) into status.
// The user only provides the suffix in spec; this is the canonical ID for all
// subsequent API calls.
if resp.GlobalReplicationGroup != nil && resp.GlobalReplicationGroup.GlobalReplicationGroupId != nil {
ko.Status.GlobalReplicationGroupID = resp.GlobalReplicationGroup.GlobalReplicationGroupId
}
if resp.GlobalReplicationGroup != nil && resp.GlobalReplicationGroup.Status != nil {
ko.Status.Status = resp.GlobalReplicationGroup.Status
}

@gustavodiaz7722 gustavodiaz7722 Sep 1, 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.

  • File: templates/hooks/global_replication_group/sdk_create_post_set_output.go.tpl, generator.yaml
  • Issue: The hook is dead code. Generated sdkCreate already assigns both values from the same response before the hook body runs; the hook re-does exactly those two assignments.
  • Fix: Delete the template file and the hooks.sdk_create_post_set_output block under GlobalReplicationGroup in generator.yaml, then regenerate.
  • Verified fix: Removing both and regenerating deletes exactly 11 lines from sdk.go and changes nothing else anywhere in the tree. The surviving generated code still sets ko.Status.GlobalReplicationGroupID (sdk.go:319) and ko.Status.Status (sdk.go:349). Controller builds.

AI-generated comment (Kiro).

gustavodiaz7722

This comment was marked as outdated.

Comment on lines +470 to +473
# --- DELETE ---
k8s.delete_custom_resource(reference)

# The delete is multi-step: disassociate secondaries (none here), then delete

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.

  • File: test/e2e/tests/test_global_replication_group.py
  • Issue: two of the most intricate new paths are never reached by a test. The suite's full inventory is six tests -- test_create_and_verify, test_invalid_primary_terminal, test_modify_description, test_modify_automatic_failover_is_noop, test_modify_multiple_fields_converges, test_scale_up_and_down -- and every one of them deletes from primary-only. Searching all 767 lines for disassoc|secondary|associate_global|engineVersion|cacheParameterGroup returns exactly one hit: the comment on this line. So delete Phase 2 (disassociate secondaries before deleting the global group, hooks.go:125-153) and the engine-upgrade branch of modifyGlobalReplicationGroup (hooks.go:378-392) are both untested. The engine branch is the one place the Engine + EngineVersion + CacheParameterGroupName grouping rule is applied, and the only path that writes the last-requested-cache-parameter-group-name annotation the resource relies on for delta correctness.
  • Fix: add test_engine_version_upgrade to TestGlobalReplicationGroupModify covering a major upgrade with engine, engineVersion, and cacheParameterGroupName patched together, asserting the AWS-side EngineVersion lands, the resource returns to Synced, and the annotation is present on the CR afterward. That is single-region and runnable in CI. For Phase 2, either add a cross-region test gated to the multi-region suite, or note in the PR description that it was validated manually and cannot run in CI.

AI-generated comment (Kiro).

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

Labels

ok-to-test Indicates a non-member PR verified by an org member that is safe to test.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support GlobalReplicationGroup in elasticache-controller

3 participants