Skip to content

Support multi-destination CRR with V2 replication configuration#2627

Open
maeldonn wants to merge 1 commit into
development/8.4from
improvement/ARSN-571/crr-multi
Open

Support multi-destination CRR with V2 replication configuration#2627
maeldonn wants to merge 1 commit into
development/8.4from
improvement/ARSN-571/crr-multi

Conversation

@maeldonn
Copy link
Copy Markdown
Contributor

Add V2 replication configuration support (Filter, Priority, per-rule Account) with multi-destination CRR. V1 remains fully supported and round-trips via a persisted format hint. Per-rule destination/role move into the Backend; legacy top-level ReplicationInfo fields kept as optional for backward-compatible reads.

Issue: ARSN-571

@maeldonn maeldonn requested review from a team, DarkIsDude and delthas May 13, 2026 17:14
@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 13, 2026

Hello maeldonn,

My role is to assist you with the merge of this
pull request. Please type @bert-e help to get information
on this process, or consult the user documentation.

Available options
name description privileged authored
/after_pull_request Wait for the given pull request id to be merged before continuing with the current one.
/bypass_author_approval Bypass the pull request author's approval
/bypass_build_status Bypass the build and test status
/bypass_commit_size Bypass the check on the size of the changeset TBA
/bypass_incompatible_branch Bypass the check on the source branch prefix
/bypass_jira_check Bypass the Jira issue check
/bypass_peer_approval Bypass the pull request peers' approval
/bypass_leader_approval Bypass the pull request leaders' approval
/approve Instruct Bert-E that the author has approved the pull request. ✍️
/create_pull_requests Allow the creation of integration pull requests.
/create_integration_branches Allow the creation of integration branches.
/no_octopus Prevent Wall-E from doing any octopus merge and use multiple consecutive merge instead
/unanimity Change review acceptance criteria from one reviewer at least to all reviewers
/wait Instruct Bert-E not to run until further notice.
Available commands
name description privileged
/help Print Bert-E's manual in the pull request.
/status Print Bert-E's current status in the pull request TBA
/clear Remove all comments from Bert-E from the history TBA
/retry Re-start a fresh build TBA
/build Re-start a fresh build TBA
/force_reset Delete integration branches & pull requests, and restart merge process from the beginning.
/reset Try to remove integration branches unless there are commits on them which do not appear on the source branch.

Status report is not available.

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 13, 2026

Incorrect fix version

The Fix Version/s in issue ARSN-571 contains:

  • None

Considering where you are trying to merge, I ignored possible hotfix versions and I expected to find:

  • 8.4.2

Please check the Fix Version/s of ARSN-571, or the target
branch of this pull request.

Comment thread lib/models/ReplicationConfiguration.ts Outdated
Comment thread lib/models/ReplicationConfiguration.ts
@claude
Copy link
Copy Markdown

claude Bot commented May 13, 2026

  • _parsePriority validates isNaN(priority) || priority < 0 but the error message says "non-negative integer" — floats like 1.5 pass through. Add !Number.isInteger(priority) to the check.
    • See inline suggestion at lib/models/ReplicationConfiguration.ts:419
  • _parseAccount accepts any non-empty string. An Account value containing colons (e.g. foo:bar) will corrupt the ARN produced by _deriveDestinationRole, since it splices the account into position 4 of a colon-delimited ARN and re-joins. At minimum reject colons; ideally validate the 12-digit AWS account format.
    • See inline suggestion at lib/models/ReplicationConfiguration.ts:616

Review by Claude Code

@maeldonn maeldonn force-pushed the improvement/ARSN-571/crr-multi branch from a4421cd to a7bbbbc Compare May 13, 2026 17:47
Comment thread lib/models/ReplicationConfiguration.ts Outdated
_derivePerRuleRoles() {
const { Rule } = this._parsedXML.ReplicationConfiguration;
this._rules!.forEach((rule, i) => {
const account = Rule[i].Destination[0].Account?.[0];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

_derivePerRuleRoles couples the _rules array ordering to the raw parsed XML Rule array by index. This works because _parseEachRule builds rules sequentially, but it is fragile — if rules are ever filtered, skipped, or reordered during parsing, the indices will silently mismatch and assign the wrong Account/role to rules.

Consider extracting the Account in _buildRuleObject (which already receives the XMLRule) and deriving the role there, so each rule carries its own Account context without depending on array-position alignment.

— Claude Code

Comment thread lib/models/ReplicationConfiguration.ts Outdated
'</ReplicationConfiguration>'
`<?xml version="1.0" encoding="UTF-8"?>` +
`<ReplicationConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">` +
`<Role>${escapeForXml(role)}</Role>` +
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

getConfigXML now emits Role before Rule elements. The old V1 code emitted them after (rulesXML + Role). While XML parsers are order-independent, this changes the byte-level output for all existing V1 configs. If any downstream consumer (e.g. checksums, string-matching assertions, ETag comparisons) depends on the exact XML string, this could be a subtle regression.

— Claude Code

})).not.toThrow();
});
});
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No round-trip test for V2: the tests verify parsing and XML generation separately, but there is no test that parses a V2 XML, extracts the config via getReplicationConfiguration(), feeds it to getConfigXML(), and re-parses the output to verify data fidelity. A round-trip test would catch any discrepancy between the two paths (e.g. lost priorities, wrong Account, prefix differences).

— Claude Code

@claude
Copy link
Copy Markdown

claude Bot commented May 13, 2026

  • _derivePerRuleRoles couples rule and XML arrays by index — fragile if rules are ever filtered or reordered during parsing - Consider moving Account extraction into _buildRuleObject to keep each rule self-contained- getConfigXML changes V1 XML element ordering (Role now before Rule elements instead of after) - Verify no downstream consumer depends on the byte-level XML output- No round-trip test for V2 (parse XML -> getReplicationConfiguration -> getConfigXML -> re-parse) - Add one to catch discrepancies between the parsing and serialization paths

    Review by Claude Code

@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

Codecov Report

❌ Patch coverage is 97.70992% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.79%. Comparing base (2312744) to head (a7bbbbc).

Files with missing lines Patch % Lines
lib/models/ReplicationConfiguration.ts 97.69% 3 Missing ⚠️
Additional details and impacted files
@@                 Coverage Diff                 @@
##           development/8.4    #2627      +/-   ##
===================================================
+ Coverage            73.55%   73.79%   +0.23%     
===================================================
  Files                  222      222              
  Lines                18214    18308      +94     
  Branches              3774     3817      +43     
===================================================
+ Hits                 13398    13510     +112     
+ Misses                4811     4793      -18     
  Partials                 5        5              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@scality scality deleted a comment from bert-e May 13, 2026
@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 13, 2026

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • 2 peers

Copy link
Copy Markdown
Contributor

@francoisferrand francoisferrand left a comment

Choose a reason for hiding this comment

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

can be greatly simplified by not being strict in parsing:

  • always parse account
  • always parse priority
  • when parsing prefix, remember if it was in a or directly in the

This will always lots of back-and-forth and coupling with the _v2 variable in parsing, and simplify generation.

(And if we really want or need to be strict in parsing, we can add an extra validation at the end of parsing : after we have parsed everything, fail if format == v1 && account && priority. But IMHO not needed...)

Comment thread lib/models/ObjectMD.ts
Comment thread lib/models/ObjectMD.ts
status: string;
dataStoreVersionId: string;
destination?: string;
role?: string;
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.

mandatory for CRR, right?

Copy link
Copy Markdown
Contributor Author

@maeldonn maeldonn May 20, 2026

Choose a reason for hiding this comment

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

Yes, but optional for backward compatibility.

Comment thread lib/models/ObjectMD.ts
@@ -1254,7 +1256,7 @@ export default class ObjectMD {
}

getReplicationTargetBucket() {
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 function still be used?
(if it is just for backwards compatibility with existing backbeat, maybe time to create a new arsenal branch, that we'll bump in the next backbeat branch)

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.

This function is now deprecated.

Comment thread lib/models/ReplicationConfiguration.ts Outdated
Comment thread lib/models/ReplicationConfiguration.ts Outdated
Comment thread lib/models/ReplicationConfiguration.ts Outdated
Comment thread lib/models/ReplicationConfiguration.ts Outdated
// Derive per-rule roles from Account after the top-level Role
// has been parsed, since role derivation depends on this._role.
if (this._format === 'v2' && this._rules) {
this._derivePerRuleRoles();
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 we do this at "parse" time (as you did), and internally store the role in each rule? or do this dynamically whenever the role is used?

- takes (some) space in metadata
- dupicates the information
+ may be slightly more efficient
+ maybe needed for better abstraction in other layers (i.e. work with "indepdendent" rules, not the whole replicationConfiguration)

Copy link
Copy Markdown
Contributor Author

@maeldonn maeldonn May 22, 2026

Choose a reason for hiding this comment

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

Switched to storing the per-rule account instead of the derived role. Top-level Role stays the single source of truth, and resolveDestinationRole() is exposed as a static helper for consumers.

Comment thread lib/models/ReplicationConfiguration.ts Outdated
* the Account field by replacing the account ID in the destination
* role from the top-level Role field.
*/
_derivePerRuleRoles() {
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 not be done directly in parseAccount? it already goes through the "account" field, if any

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.

Can't be done in parseAccount, at that point the role hasn't been parsed yet. The derivation needs the top-level Role, so it's deferred once both are known.

Comment thread lib/models/ReplicationConfiguration.ts Outdated
if (account) {
const destRole = this._deriveDestinationRole(account);
if (destRole) {
rule.role = destRole;
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.

not stored everytime (e.g. if no account) : so code using role still needs to be aware, cannot be simplified that much...

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.

resolveDestinationRole returns the right ARN whether or not the rule has an account.

Comment thread lib/models/ReplicationConfiguration.ts Outdated
// rules (and never for V1).
const isV2 = format
? format === 'v2'
: rules.some(r => r.destination !== undefined);
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.

seems weird to couple the output format to our internal representation.
in both AWS v1/v2 there is a Destination, so should not be used to distinguish...

at the same time, the difference between v1 and v2 are really about prefix.

while we could argue about being strict on parsing, there is no such problem here:

  • we can generate priority if set (it won't be set on v1 if not allowed, no point depending on format)
  • we can generate account if set (it won't be set on v1 if not allowed, no point depending on format)
  • we need to know the format for prefix, but only if a prefix was actually set: if it was not don't generate anything (no prefix or filter), and if it was we know the format

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.

Updated.

Copy link
Copy Markdown
Contributor

@DarkIsDude DarkIsDude left a comment

Choose a reason for hiding this comment

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

Not much more than Francois 🙏

Comment thread lib/models/ReplicationConfiguration.ts Outdated
Comment thread lib/models/ReplicationConfiguration.ts
@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 18, 2026

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • 2 peers

The following reviewers are expecting changes from the author, or must review again:

@delthas delthas removed their request for review May 18, 2026 14:04
Add V2 replication configuration format with Filter, Priority, and
per-rule Account/Destination fields. V1 format remains fully supported.

Issue: ARSN-571
@maeldonn maeldonn force-pushed the improvement/ARSN-571/crr-multi branch from a7bbbbc to acc58f0 Compare May 22, 2026 12:37
* pair; the destination side is used when present. If the rule carries
* an `account` override, its 12-digit ID replaces the account segment.
*/
static resolveDestinationRole(topRole: string, account?: string): string | undefined {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

resolveDestinationRole is a new public static method with non-trivial ARN-splitting and account-substitution logic, but has no direct unit tests. Consider adding tests for: single ARN without account, comma-separated pair, account override replacing the correct segment, and empty/falsy topRole.

— Claude Code

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented May 22, 2026

Incorrect fix version

The Fix Version/s in issue ARSN-571 contains:

  • 8.4.2

Considering where you are trying to merge, I ignored possible hotfix versions and I expected to find:

  • 8.4.3

Please check the Fix Version/s of ARSN-571, or the target
branch of this pull request.

obj.storageClass = storageClass;
}

if (rule.Priority?.[0] !== undefined) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

_parsePriority skips validation when rule.Priority[0] is an empty string (!"" is true), but _buildRuleObject still converts it to a number: +'' → 0. An empty element would silently set priority to 0 without validation.

```suggestion
if (rule.Priority?.[0] !== undefined && rule.Priority[0] !== '') {

@@ -144,6 +175,7 @@ export default class ReplicationConfiguration {
destination: this.getDestination(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

getDestination() returns string | null (always null now since _destination is never set), but ReplicationConfigurationMetadata.destination is typed as string | undefined. Under strict: true, assigning null to string | undefined is a type error. Consider changing _destination to string | undefined and initializing to undefined, or adjusting the return type here.

— Claude Code

@claude
Copy link
Copy Markdown

claude Bot commented May 22, 2026

  • Empty <Priority></Priority> silently becomes priority 0: _parsePriority skips empty strings but _buildRuleObject still parses them via +''
    • Guard with rule.Priority?.[0] !== undefined && rule.Priority[0] !== ''
  • getDestination() returns null but ReplicationConfigurationMetadata.destination is typed string | undefined — type mismatch under strict: true
    • Change _destination init to undefined instead of null, or adjust the metadata type
  • resolveDestinationRole is a new public static method with no direct test coverage
    • Add tests for single ARN, comma pair, account override, and empty input

Review by Claude Code

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants