fix(agent): default S3 RequestChecksumCalculation to WhenRequired#342
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThe S3 client and snapshot transfer manager now share a required-only request checksum policy. Tests verify checksum headers for single-part and multipart uploads, while documentation describes direct streaming and troubleshooting for incompatible S3 backends. ChangesS3 checksum handling
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant SnapshotAgent
participant S3TransferManager
participant S3Backend
SnapshotAgent->>S3TransferManager: upload snapshot stream
S3TransferManager->>S3Backend: send single-part or multipart requests
S3Backend-->>S3TransferManager: return upload responses
S3TransferManager-->>SnapshotAgent: return upload result
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request configures the S3 client to only calculate request checksums when required (RequestChecksumCalculationWhenRequired). This prevents upload failures on S3-compatible backends (such as Ceph RGW, MinIO, and Cloudflare R2) that reject the default flexible checksums on PutObject operations. Additionally, a unit test has been added to verify this configuration. There are no review comments, and I have no further feedback to provide.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
55ae956 to
17d9ed0
Compare
Timofei Larkin (lllamnyp)
left a comment
There was a problem hiding this comment.
Thanks for chasing this down — the diagnosis of why S3-compatible backends reject the upload is right, and the root-cause writeup is genuinely useful. But the fix as written does not reach the code path that uploads real snapshots, so the reported failure survives this PR for any snapshot above 5 MiB. Details below.
Blocker 1 — the fix does not reach the code path that actually uploads snapshots
internal/agent/agent.go:180 sets RequestChecksumCalculation = WhenRequired on the S3 client options. But the snapshot upload never issues a bare client.PutObject for a realistically-sized snapshot — internal/agent/snapshot.go:240 goes through the transfer manager:
return uploadStreamHashed(ctx, manager.NewUploader(client), &s3.PutObjectInput{...}, body)manager.Uploader carries its own RequestChecksumCalculation field, and NewUploader hardcodes it to WhenSupported — it never reads the client's options (feature/s3/manager@v1.22.22/upload.go:318):
u := &Uploader{
S3: client,
PartSize: DefaultUploadPartSize,
...
RequestChecksumCalculation: aws.RequestChecksumCalculationWhenSupported,
}For a body larger than DefaultUploadPartSize (5 MiB) the uploader takes the multipart branch, and initChecksumAlgorithm (upload.go:834-855) then stamps ChecksumAlgorithm = CRC32 onto CreateMultipartUpload and every UploadPart:
default:
if u.cfg.RequestChecksumCalculation != aws.RequestChecksumCalculationWhenRequired {
u.in.ChecksumAlgorithm = types.ChecksumAlgorithmCrc32
}An explicitly-set ChecksumAlgorithm input member then bypasses the client-level setting entirely — SetupInputContext.HandleInitialize (service/internal/checksum@v1.9.18/middleware_setup_context.go:51-55) returns early with the algorithm taken from the input and never consults RequestChecksumCalculation:
if m.GetAlgorithm != nil {
if algorithm, ok := m.GetAlgorithm(in.Parameters); ok {
ctx = internalcontext.SetChecksumInputAlgorithm(ctx, algorithm)
return next.HandleInitialize(ctx, in)
}
}UploadPart is registered with EnableTrailingChecksum: true (service/s3@v1.102.2/api_op_UploadPart.go:688-695), so over HTTPS the compute-input-checksum middleware takes the trailer path and sets x-amz-content-sha256: STREAMING-UNSIGNED-PAYLOAD-TRAILER — exactly the header this PR quotes Ceph RGW rejecting.
Verified empirically on this branch. Building the client via destination.s3Client exactly as production does (i.e. with this PR's fix applied) and the uploader via manager.NewUploader(client) exactly as uploadS3Stream does, then uploading a 12 MiB body at a local TLS test server:
POST /b/k?uploads= sha256=e3b0c442... trailer= algo=
PUT /b/k?partNumber=1&uploadId=uid&x-id=UploadPart sha256=STREAMING-UNSIGNED-PAYLOAD-TRAILER trailer=x-amz-checksum-crc32 algo=CRC32
PUT /b/k?partNumber=2&uploadId=uid&x-id=UploadPart sha256=STREAMING-UNSIGNED-PAYLOAD-TRAILER trailer=x-amz-checksum-crc32 algo=CRC32
PUT /b/k?partNumber=3&uploadId=uid&x-id=UploadPart sha256=STREAMING-UNSIGNED-PAYLOAD-TRAILER trailer=x-amz-checksum-crc32 algo=CRC32
With the uploader option added (below), the same run produces:
PUT /b/k?partNumber=1&uploadId=uid&x-id=UploadPart sha256=UNSIGNED-PAYLOAD trailer= algo=
PUT /b/k?partNumber=2&uploadId=uid&x-id=UploadPart sha256=UNSIGNED-PAYLOAD trailer= algo=
PUT /b/k?partNumber=3&uploadId=uid&x-id=UploadPart sha256=UNSIGNED-PAYLOAD trailer= algo=
Practical impact: a real etcd snapshot is essentially never under 5 MiB, so as merged this PR fixes only the degenerate tiny-snapshot case and leaves the reported failure fully intact for real clusters.
A note on the verification in the description: setting AWS_REQUEST_CHECKSUM_CALCULATION=when_required lands in aws.Config → s3.Options, and manager.NewUploader ignores that too. If the Ceph RGW run really did succeed end-to-end, something other than the checksum trailer explains it and the diagnosis is worth revisiting.
Fix — set the option on the uploader as well, in internal/agent/snapshot.go:240:
uploader := manager.NewUploader(client, func(u *manager.Uploader) {
u.RequestChecksumCalculation = aws.RequestChecksumCalculationWhenRequired
})
return uploadStreamHashed(ctx, uploader, &s3.PutObjectInput{...}, body)Keep the client-level setting too — it covers PutObject on the single-part path and HeadObject.
On the two settings. Needing to set the same intent in two independent places is a footgun: either one can be deleted later and the bug comes back silently, with the multipart case being the one that isn't obvious. Worth making that hard to get wrong — derive both from a single package-level constant at minimum, and have the comment state explicitly that the client option and the uploader option are separate knobs that must agree. (Unrelated but adjacent: manager is now deprecated in favour of feature/s3/transfermanager, so this duplication may be worth revisiting wholesale rather than patching twice.)
Test that fails without the fix. In internal/agent/snapshot_test.go, stand up an httptest.NewTLSServer answering ?uploads with an InitiateMultipartUploadResult, PUT ...uploadId= with 200 + ETag, and POST ...uploadId= with a CompleteMultipartUploadResult, recording each request's x-amz-content-sha256, x-amz-trailer and x-amz-sdk-checksum-algorithm. Point destination{kind:"s3", s3Endpoint: srv.URL, s3PathStyle:true} at it (overriding HTTPClient with srv.Client()), run uploadS3Stream with a >5 MiB reader, and assert no request carries x-amz-content-sha256: STREAMING-UNSIGNED-PAYLOAD-TRAILER, no x-amz-trailer, and no x-amz-sdk-checksum-algorithm. That test fails on the current branch and passes with the uploader option. Anything cheaper — asserting only on s3.Options — does not exercise the path that broke.
Blocker 2 — the code comment asserts something the code does not do, and that assertion is what hides Blocker 1
internal/agent/agent.go:178-179:
"WhenRequired keeps uploads working on those backends and is equally accepted by real AWS S3, which does not require a checksum on a plain PutObject."
The agent does not issue a plain PutObject for anything of realistic size — snapshot.go:228-245 hands the stream to manager.Uploader, which multiparts above 5 MiB. The comment, and the matching claim in the description that a plain PutObject is "all the agent ever issues", is wrong about the very operation the change is meant to fix. Please rewrite it to say that the agent uploads via the transfer manager, that the manager multiparts above 5 MiB, and that the manager carries its own RequestChecksumCalculation that must be set alongside the client option.
The Blocker 1 test doubles as the guard here — it pins the multipart behaviour the comment describes.
Blocker 3 — stale documentation in the exact area being changed
docs/concepts.md:452 describes the snapshot agent as streaming the snapshot "to a local file (PVC destination) or a temp file it then multipart-uploads to S3". The S3 path stages nothing: snapshot.go:159-167 streams straight to S3, and its comment says so explicitly — "Stream straight to S3 — no local staging. The snapshot Pod has no sized volume…". Staging-to-disk exists only on the restore side. Since this PR changes the S3 upload configuration, please correct the sentence describing that upload to something like "streams directly to S3 via the transfer manager (multipart above 5 MiB), with no local staging".
This is prose, so the guard is the Blocker 1 multipart test plus the existing TestUploadStreamHashedStreamsThrough in internal/agent/snapshot_test.go, which already proves no staging occurs.
Recommended to fix
internal/agent/agent_test.go:39-63— the new test asserts a setting the production upload path overrides. As written,TestS3ClientChecksumWhenRequiredpasses green while the reported bug is fully live. Keep it as a unit check ons3Clientif you like, but it must not be the only checksum test in the PR; the multipart test from Blocker 1 is the one carrying the contract.internal/agent/agent_test.go:43— non-hermetic unit test.d.s3Client(ctx)callsawsconfig.LoadDefaultConfig, which readsAWS_*env vars and~/.aws/config; a runner withAWS_PROFILEpointing at a nonexistent profile fails this test for reasons unrelated to the assertion. Addt.Setenv("AWS_CONFIG_FILE", "/dev/null"),t.Setenv("AWS_SHARED_CREDENTIALS_FILE", "/dev/null")andt.Setenv("AWS_PROFILE", "").- Hardcoding removes the escape hatch.
s3.NewFromConfigcopiesaws.Config.RequestChecksumCalculation(populated byLoadDefaultConfigfromAWS_REQUEST_CHECKSUM_CALCULATION/request_checksum_calculation) intos3.Options, and the new line unconditionally overwrites it. That is very likely the intent — the agent's environment is a closed list built by the controller, so no operator could set that var on the Pod anyway — but one sentence in the comment would save the next person the archaeology. internal/agent/restore.go:273— consider the symmetric setting on the download side.manager.NewDownloader(client)issues rangedGetObjects withChecksumMode: ENABLED(client defaultResponseChecksumValidation = WhenSupported). Not the reported failure and usually harmless, but the same class of AWS-default-vs-S3-compatible mismatch, and untested. Either seto.ResponseChecksumValidation = aws.ResponseChecksumValidationWhenRequirednext to the request-side setting with an httptest-based restore test asserting nox-amz-checksum-modeheader, or say in the comment why it is deliberately left alone.
Note on the integrity trade-off
Worth stating plainly in the description, since the current text slightly oversells it. After this change there is no server-side verification of the uploaded bytes at all. uploadStreamHashed (snapshot.go:256) does tee the body through sha256, but that digest is computed over what the agent sent — it is never compared against anything server-side, and never re-verified on the way back, since restore.go:148 sets SkipHashCheck (deliberately, as a streamed snapshot carries no etcd hash footer). With WhenSupported the backend validated each part's CRC32 and would have rejected a corrupted part.
Concretely: with an endpoint configured as plain http:// behind a proxy that flips a byte, today you get BadDigest and a failed backup; after this change the object stores corrupt, the recorded sha256 matches the pre-corruption stream so status looks healthy, and it surfaces only at restore where the hash check is skipped. I don't think that blocks the change — the alternative is backups that don't work at all — but a good follow-up would be verifying the recorded sha256 against the downloaded object in downloadS3, which restores an end-to-end guard independent of any SDK checksum setting.
Title
The change is global; nothing distinguishes AWS from non-AWS endpoints. Suggest dropping "for non-AWS backends" from the title, or rewording to "…to unbreak non-AWS backends", so nobody goes looking for the conditional. Similarly, the comment names "some MinIO and Cloudflare R2 versions" while only Ceph RGW was confirmed — worth naming RGW as the verified case and softening the rest.
…e client Review of #342 found the client-only option never reached the code path that uploads real snapshots. manager.Uploader carries its OWN RequestChecksumCalculation (NewUploader hardcodes WhenSupported) and never reads the s3.Client option, so any snapshot over the manager's 5 MiB part size — every real etcd snapshot — took the multipart branch and re-stamped the CRC32 STREAMING-UNSIGNED-PAYLOAD-TRAILER that Ceph RGW rejects. The previous fix only covered the degenerate sub-5-MiB single-part case. - Derive both knobs from one package-level constant (snapshotChecksumCalculation) and set it on the uploader via newSnapshotUploader as well as the client, so the two independent settings cannot silently drift. - Add TestUploadS3StreamMultipartNoChecksumTrailer: drives the real uploadS3Stream against a TLS httptest server (trusted via AWS_CA_BUNDLE) with a >5 MiB body and asserts no UploadPart carries a checksum trailer/algorithm. Fails without the uploader option; a check on s3.Options alone stays green while the bug is live. - Make TestS3ClientChecksumWhenRequired hermetic (AWS_CONFIG_FILE/… = /dev/null, empty AWS_PROFILE) and assert the response-side setting; keep it as a client-option unit check only. - Set ResponseChecksumValidation=WhenRequired symmetrically for the restore download path (same mismatch class; defensive, the downloader sets no ChecksumMode today). - Rewrite the agent.go comment that wrongly claimed a plain PutObject, and fix the stale "temp file it then multipart-uploads" sentence in docs/concepts.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
|
Thanks — the diagnosis is exactly right, and I could reproduce the trailer trace. Pushed e1c9abd addressing all three blockers plus the recommended items. Blocker 1 — fix now reaches the multipart path
Confirmed it fails on the pre-fix uploader and passes with it — the failing run reproduces your trace verbatim: Blocker 2 — comment correctedThe Blocker 3 — stale docs fixed
Recommended
Integrity trade-offAgreed and worth stating: after this there's no server-side verification of the uploaded bytes, and the recorded sha256 is over what the agent sent, never re-checked (restore uses Title / verification noteRetitled to drop "for non-AWS backends" (the change is global). On the description's RGW claim: the manual canary that succeeded uploaded an 86 KB object — under the 5 MiB part size, so it took the single-part |
aws-sdk-go-v2 defaults RequestChecksumCalculation to WhenSupported (since early 2025), attaching a flexible checksum to every PutObject. Non-AWS S3-compatible backends (Ceph RGW, some MinIO/R2) reject the accompanying header with "InvalidArgument: x-amz-content-sha256 must be UNSIGNED-PAYLOAD, STREAMING-AWS4-HMAC-SHA256-PAYLOAD or a valid sha256 value", so the snapshot-agent reads the etcd snapshot fine but fails at S3 upload. Set the S3 client to WhenRequired so a checksum is only added when the operation requires one; a plain PutObject then carries none, which both AWS S3 and the affected backends accept. No API/CRD change and no new knob. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
…e client Review of #342 found the client-only option never reached the code path that uploads real snapshots. manager.Uploader carries its OWN RequestChecksumCalculation (NewUploader hardcodes WhenSupported) and never reads the s3.Client option, so any snapshot over the manager's 5 MiB part size — every real etcd snapshot — took the multipart branch and re-stamped the CRC32 STREAMING-UNSIGNED-PAYLOAD-TRAILER that Ceph RGW rejects. The previous fix only covered the degenerate sub-5-MiB single-part case. - Derive both knobs from one package-level constant (snapshotChecksumCalculation) and set it on the uploader via newSnapshotUploader as well as the client, so the two independent settings cannot silently drift. - Add TestUploadS3StreamMultipartNoChecksumTrailer: drives the real uploadS3Stream against a TLS httptest server (trusted via AWS_CA_BUNDLE) with a >5 MiB body and asserts no UploadPart carries a checksum trailer/algorithm. Fails without the uploader option; a check on s3.Options alone stays green while the bug is live. - Make TestS3ClientChecksumWhenRequired hermetic (AWS_CONFIG_FILE/… = /dev/null, empty AWS_PROFILE) and assert the response-side setting; keep it as a client-option unit check only. - Set ResponseChecksumValidation=WhenRequired symmetrically for the restore download path (same mismatch class; defensive, the downloader sets no ChecksumMode today). - Rewrite the agent.go comment that wrongly claimed a plain PutObject, and fix the stale "temp file it then multipart-uploads" sentence in docs/concepts.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
e1c9abd to
a8bc4ed
Compare
Timofei Larkin (lllamnyp)
left a comment
There was a problem hiding this comment.
Thanks for the rework — the request-side fix is now right, and the two-knob structure is exactly what was needed. Confirmed by mutation test rather than reading: deleting the manager.Uploader option makes TestUploadS3StreamMultipartNoChecksumTrailer fail with precisely the production symptom (x-amz-content-sha256=STREAMING-UNSIGNED-PAYLOAD-TRAILER, x-amz-trailer=x-amz-checksum-crc32, x-amz-sdk-checksum-algorithm=CRC32 on every UploadPart). go test ./internal/agent/, go vet, gofmt -l all clean. The single constant feeding both sites is the right shape, docs/concepts.md:452 is now accurate against snapshot.go:159-168, and the "closed env list" claim at agent.go:199-203 holds — controllers/snapshot_job.go:145-159, controllers/etcdmember_controller.go:866-880 and internal/migrate/snapshotjob.go:118-129 all build Env from fixed lists with no EnvFrom.
Three things still block, all in the new response-side rider and its coverage.
Blocking
1. ResponseChecksumValidation = WhenRequired is unjustified, mis-explained and untested — internal/agent/agent.go:205-209, internal/agent/restore.go:273-277.
The comments justify it as avoiding "demanding a response checksum the backend may not emit". The SDK default does not do that. Three source facts, each checked against the pinned versions:
service/internal/checksum@v1.9.18/middleware_setup_context.go:95— the defaultWhenSupportedonly setsChecksumMode=ENABLEDon the request. It demands nothing.middleware_validate_output.go:101-111— if the response carries no checksum header, validation is skipped and the middleware returns cleanly:if len(expectedChecksum) == 0 || len(algorithmToUse) == 0 { … return out, metadata, nil }.service/s3@v1.102.2/api_op_GetObject.go:861—GetObjectis registered withIgnoreMultipartValidation: true, so composite checksums (the…-Nform every multipart-uploaded object carries, i.e. every snapshot this agent writes) are skipped rather than rejected.
So the failure mode the comment describes cannot occur, and the line isn't needed to fix the reported bug. What it actually does is stop validating checksums that are returned — and the restore path has no other integrity check to fall back on: downloadS3 (restore.go:258-283) never hashes what it receives, nothing compares status.artifact.checksum, and the restore itself runs with SkipHashCheck (restore.go:148, and documented at docs/concepts.md:457). With the request-side checksum also off, a corrupted object now has no detection point anywhere in the round trip.
Either fix works:
- Drop the line and both comments, keeping the PR scoped to the observed request-side bug. Test: assert
o.ResponseChecksumValidation == aws.ResponseChecksumValidationWhenSupportedinagent_test.go:57-70— that assertion fails today, which is what makes it a guard. - Or keep it and add real verification in its place: tee
downloadS3through sha256 and compare against the recorded digest (plumbed alongside the otherSNAPSHOT_*env vars atetcdmember_controller.go:866-880). Test:TestDownloadS3ChecksumMismatchagainst anhttptestTLS server serving a body whose digest differs from the expected one, asserting both an error and removal of the staged file.
The first is the smaller change and I'd suggest it; the second is more valuable if you want it.
2. The single-part upload path has no end-to-end guard — agent.go:204, snapshot_test.go:89-186.
TestUploadS3StreamMultipartNoChecksumTrailer uses a 12 MiB body and so drives only the multipart branch. Below the 5 MiB part size the transfer manager issues a single-part PutObject, where the client option is the only thing keeping the trailer off — and that option is currently pinned by a struct-field comparison alone (agent_test.go:66). That is the same class of check that stayed green through 61144d4 while the real path was broken.
This is not hypothetical: a snapshot of a small or freshly-bootstrapped cluster is tens of KiB and takes the single-part path in production, against the same RGW endpoint. I probed the current branch with a throwaway 1 MiB upload and the behaviour is correct today — PUT /b/k?x-id=PutObject sha=UNSIGNED-PAYLOAD trailer= algo= — but nothing pins it.
Fix: add TestUploadS3StreamSinglePartNoChecksumTrailer reusing the same handler and CA setup with a ~1 MiB body, asserting no X-Amz-Trailer, no X-Amz-Sdk-Checksum-Algorithm, and X-Amz-Content-Sha256 != STREAMING-UNSIGNED-PAYLOAD-TRAILER, plus a guard that exactly one PUT was seen carrying no partNumber (so the test fails loudly if the body ever stops taking the single-part branch). Removing the client option must make it fail.
3. Comments overstate which path is affected — agent.go:161-164, snapshot.go:250-253.
Both say multipart is taken "for every real etcd snapshot" and call it "the one that matters". Small clusters take the single-part path, where the client option is load-bearing. A reader who trusts this wording is one cleanup away from deleting the client option and reintroducing the RGW failure for exactly the clusters most likely to be someone's first try. Suggest "any body over the 5-MiB part size — every snapshot of a non-trivial cluster", and dropping "the one that matters". Issue 2's test is what makes the corrected statement enforceable.
Recommended to fix
agent.go:151,176—snapshotChecksumCalculationalso governs the restore/download client (restore.go:241,261), so thesnapshotprefix is misleading. Suggests3RequestChecksumCalculation.docs/operations.md:269— worth a sibling troubleshooting line next to the existingListBucket/403 note, so an operator hittingInvalidArgument: x-amz-content-sha256 must be UNSIGNED-PAYLOAD…finds the answer in the docs rather than only in a Go comment.snapshot_test.go:113,118—io.WriteString(w, …)returns are dropped while_, _ = io.Copy(…)two lines above is explicit; pick one convention.
Clean
No leaked secrets — test credentials are the literal test against loopback. hermeticAWSEnv properly isolates ~/.aws, AWS_PROFILE and the metadata endpoint. All four S3 construction sites in the repo (snapshot.go:229, restore.go:241,261,278) route through destination.s3Client, so no path is left unpatched.
… path Addresses the three blockers from the second review, all on the current HEAD: - ResponseChecksumValidation = WhenRequired was unjustified and untested. The SDK default (WhenSupported) demands nothing: it validates a checksum only when the backend returns one (middleware_validate_output.go returns cleanly when absent) and GetObject registers IgnoreMultipartValidation, so the composite checksum every multipart-uploaded snapshot carries is skipped anyway. Pinning it to WhenRequired fixed no real failure and discarded the only server-side integrity signal the restore path has (downloadS3 runs with SkipHashCheck). Dropped the line and both comments; the client-option test now asserts the response side stays at the WhenSupported default, so re-pinning it fails. - The single-part upload path had no end-to-end guard. A small or freshly bootstrapped cluster's snapshot is under the 5 MiB part size, so the transfer manager issues a single-part PutObject where the s3.Client option is the only thing keeping the trailer off — previously pinned by a struct-field check alone, the same class of assertion that stayed green in 61144d4 while every real snapshot was broken. Added TestUploadS3StreamSinglePartNoChecksumTrailer (1 MiB body) asserting no checksum trailer and exactly one single-part PUT; removing the client option makes it fail. - Comments overstated multipart as "every real etcd snapshot" / "the one that matters", nudging a future maintainer to delete the load-bearing client option. Reworded to "any body over the 5 MiB part size — every snapshot of a non-trivial cluster" and dropped "the one that matters". Also renamed snapshotChecksumCalculation -> s3RequestChecksumCalculation (it governs the restore/download client too), added an operations.md troubleshooting note for the InvalidArgument x-amz-content-sha256 error, and factored the fake S3 endpoint into a shared test helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
|
Thanks — all three confirmed against the pinned SDK sources and addressed in 562193d. Blocker 1 — response-side pin droppedYou're right on every point. Went with the smaller change: dropped Blocker 2 — single-part path now guarded end-to-endAdded Blocker 3 — comments correctedBoth sites now read "any body over the 5-MiB part size — every snapshot of a non-trivial cluster", and "the one that matters" is gone. The constant comment now states plainly that both sites are load-bearing — the client option for the single-part path + HeadObject, the uploader option for multipart parts. Recommended (all done)
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
internal/agent/snapshot.go (1)
255-259: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoffConsider migrating this helper to
feature/s3/transfermanager. The newer package isn’t ingo.modyet, so this would require adding a dependency as well as updating the uploader helper.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/agent/snapshot.go` around lines 255 - 259, Update newSnapshotUploader to use the newer feature/s3/transfermanager package, add its dependency to go.mod, and preserve the existing request checksum configuration when constructing the uploader.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@internal/agent/snapshot.go`:
- Around line 255-259: Update newSnapshotUploader to use the newer
feature/s3/transfermanager package, add its dependency to go.mod, and preserve
the existing request checksum configuration when constructing the uploader.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1bb849ce-832b-4a34-a8fb-2297f635c667
📒 Files selected for processing (6)
docs/concepts.mddocs/operations.mdinternal/agent/agent.gointernal/agent/agent_test.gointernal/agent/snapshot.gointernal/agent/snapshot_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
- docs/concepts.md
- internal/agent/agent_test.go
The comment on the s3.Client options claimed ResponseChecksumValidation is
"the only server-side integrity signal the restore path has, since downloadS3
runs with SkipHashCheck". Both halves were wrong:
- SkipHashCheck is a field of etcdutl's snapshot.RestoreConfig (restore.go),
covering etcd's own appended snapshot hash. downloadS3 passes no such
option and has no equivalent.
- The signal it credits is inert for objects this agent writes: response
validation fires only when the backend returns an x-amz-checksum-* header,
and pinning requests to WhenRequired means no checksum is ever stored.
Leaving ResponseChecksumValidation at the SDK default is still right, so the
decision stands; only the reason changes. The same wrong text was duplicated
in the test comment and is corrected there too.
Also trim the rationale to one home on s3RequestChecksumCalculation with
pointers from the use sites, drop a commit hash that will not survive a
squash-merge, and use os.DevNull instead of a hardcoded /dev/null.
No behavior change: both mutation checks still fail closed (drop the client
option and the single-part test fails; drop the uploader option and the
multipart test fails).
Assisted-By: Claude Opus 4.8 (1M context)
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
Timofei Larkin (lllamnyp)
left a comment
There was a problem hiding this comment.
All three rounds of findings are addressed and verified.
The request-checksum pin is correct at both load-bearing sites — the s3.Client option for the single-part PutObject/HeadObject path, and manager.Uploader.RequestChecksumCalculation for the multipart parts, which NewUploader hardcodes to WhenSupported and never reads from the client. Both derive from one constant, so they cannot drift.
Both guards are mutation-tested rather than assumed: dropping the client option fails TestUploadS3StreamSinglePartNoChecksumTrailer, dropping the uploader option fails TestUploadS3StreamMultipartNoChecksumTrailer, each with the exact production symptom (x-amz-content-sha256: STREAMING-UNSIGNED-PAYLOAD-TRAILER, x-amz-trailer: x-amz-checksum-crc32). ResponseChecksumValidation is left at the SDK default, with a test asserting it stays there.
Thanks for the thorough iteration on this one.
…ends) (#3403) ## What this PR does Bumps the etcd-operator to **v0.5.3**, which carries the S3 request-checksum fix ([cozystack/etcd-operator#342](cozystack/etcd-operator#342)). `git compare v0.5.2...v0.5.3` is exactly #340, #341, #342. **Why:** scheduled etcd snapshots to non-AWS S3-compatible backends (**Ceph RGW** confirmed) fail at upload. Since early 2025 `aws-sdk-go-v2` defaults `RequestChecksumCalculation` to `WhenSupported`, which stamps a CRC32 on every upload; over HTTPS a multipart part rides it as `x-amz-content-sha256: STREAMING-UNSIGNED-PAYLOAD-TRAILER`, and RGW rejects it with `400 InvalidArgument`. v0.5.3 sets `WhenRequired` on both the S3 client and the transfer manager, so multipart uploads (>5 MiB — every real etcd snapshot) carry no checksum trailer. **Changes:** - `system/etcd-operator` — `appVersion` v0.5.2 → v0.5.3 (image tag follows AppVersion; `values.yaml` keeps `tag: ""`), bump `ETCD_OPERATOR_REF`, update deployment unittest expectations. - `system/etcd-operator-crds` — re-vendor CRDs from v0.5.3. `etcdmembers` gains the additive #340 `status.version` field + **Running** printer column and picks up the `peerAutoTLS` spec field the vendored copy was already missing (all additive, backward-compatible; `etcdclusters`/`etcdsnapshots` unchanged). Fixes the stale `make update` (upstream has no `config/crd` kustomization — CRDs live in `charts/etcd-operator/crd-bases`) and makes it fail loudly on a fetch error (temp file under `set -e`, not a pipe). **Verification:** `helm unittest` 18/18 and `helm template` green on both packages; regenerated CRDs are byte-identical to upstream `crd-bases` at v0.5.3. End-to-end on a live Ceph RGW backend (freedom-portal-stage): the v0.5.2 (client-only) agent fails a multipart snapshot upload with the 400; v0.5.3 uploads an 11 MiB multipart snapshot successfully, and the operator-driven CronJob→EtcdSnapshot path reaches `Complete`. ### Downstream repositories Walked the trigger map in `docs/agents/contributing.md` file-by-file against the diff (`packages/system/etcd-operator{,-crds}/**` only — a component version bump + additive CRD re-vendor). Nothing matches: not an `apps/`/`extra/` package add/rename/remove, no `core/platform` values, no variant/bundle, no platform component add/remove, no `hack/` layout or shared-tooling change, and the provider does not type the `etcd-operator.cozystack.io` CRDs (and the change is additive/opaque regardless). - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: ### Release note ```release-note fix(etcd-operator): bump to v0.5.3 — etcd snapshot uploads to non-AWS S3-compatible backends (Ceph RGW, some MinIO/R2) no longer fail with `400 InvalidArgument: x-amz-content-sha256 ...`; the snapshot agent now requests a checksum only when required, on both the S3 client and the multipart transfer manager. Also brings observed EtcdMember versions (#340) and `--watch-namespace` (#341). ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added runtime etcd version visibility to `EtcdMember` (including a “Running” status column). * Added `peerAutoTLS` support to `EtcdMember` for operator-managed peer TLS behavior. * **Updates** * Updated the etcd-operator to version 0.5.3. * Refreshed bundled CRDs and ensured CRDs are preserved during Helm lifecycle operations. * **Tests** * Updated deployment test expectations to use the 0.5.3 operator image. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
…ends) v0.5.3 carries the S3 request-checksum fix (cozystack/etcd-operator#342): the snapshot agent sets RequestChecksumCalculation=WhenRequired on BOTH the S3 client and the transfer manager, so multipart snapshot uploads (>5 MiB — every real etcd snapshot) no longer carry the CRC32 STREAMING-UNSIGNED-PAYLOAD-TRAILER that Ceph RGW rejects with 400 InvalidArgument. Also pulls in cozystack#340 (observed EtcdMember versions) and cozystack#341 (--watch-namespace). - system/etcd-operator: appVersion v0.5.2 -> v0.5.3 (image tag follows AppVersion), bump ETCD_OPERATOR_REF, update deployment unittest expectations. - system/etcd-operator-crds: re-vendor CRDs from v0.5.3. etcdmembers gains the additive cozystack#340 status.version field + "Running" printer column and picks up the peerAutoTLS spec field the vendored copy was already missing (all additive, backward-compatible; etcdclusters/etcdsnapshots unchanged). Fix the stale `make update`: upstream has no config/crd kustomization — CRDs live in charts/etcd-operator/crd-bases — so vendor those directly and stamp helm.sh/resource-policy: keep. Verified end-to-end on a Ceph RGW backend: the v0.5.2 (client-only) agent fails a multipart snapshot upload with the 400 above; v0.5.3 uploads an 11 MiB multipart snapshot successfully. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
The barman-cloud plugin's sidecar uploads via boto3. Since botocore ~1.36 (early 2025) the default RequestChecksumCalculation is when_supported, which attaches a flexible checksum to every PutObject. Non-AWS S3-compatible backends (Ceph RADOS Gateway, some MinIO / Cloudflare R2 builds) reject the accompanying header with "InvalidArgument: x-amz-content-sha256 must be UNSIGNED-PAYLOAD, STREAMING-AWS4-HMAC-SHA256-PAYLOAD or a valid sha256 value", so every backup/WAL-archive upload fails against them. Pin the sidecar to when_required via the ObjectStore's spec.instanceSidecarConfiguration.env (botocore honors the AWS_REQUEST_CHECKSUM_CALCULATION env var) on both the keycloak system DB and the postgres app (backup + recovery ObjectStores). when_required is also accepted by AWS S3 on a plain PutObject, so it is a safe default everywhere. This mirrors the etcd-operator fix (cozystack/etcd-operator#342, released in v0.5.3), which applied the same WhenRequired policy to its aws-sdk-go-v2 S3 client for the same Ceph RGW backend. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Andrey Kolkov <andrey.kolkov@aenix.io>
… (non-AWS S3 / Ceph RGW) (#3417) ## What this PR does Fixes CNPG **barman-cloud plugin** backups to non-AWS, S3-compatible object stores (Ceph RADOS Gateway, and some MinIO / Cloudflare R2 builds). The plugin's sidecar uploads via **boto3**. Since botocore ~1.36 (early 2025) the default `RequestChecksumCalculation` is `when_supported`, so a flexible checksum (CRC32) plus the corresponding `x-amz-content-sha256` handling is attached to every `PutObject`. AWS S3 accepts it, but several S3-compatible backends reject the request: ``` InvalidArgument: x-amz-content-sha256 must be UNSIGNED-PAYLOAD, STREAMING-AWS4-HMAC-SHA256-PAYLOAD or a valid sha256 value. ``` so every backup / WAL-archive upload fails against them and the `ScheduledBackup` never stores anything. This sets `spec.instanceSidecarConfiguration.env` on the rendered `ObjectStore`s to carry `AWS_REQUEST_CHECKSUM_CALCULATION=when_required` (botocore reads this env var from the sidecar's environment). `when_required` computes a checksum only for operations that mandate one; a plain `PutObject` then carries none, which both AWS S3 and the affected backends accept — a safe default everywhere. No API/CRD change, no new value, no new dependency. Applied to every barman-cloud `ObjectStore` Cozystack creates: - **chart-rendered** — `packages/system/keycloak/templates/db.yaml` (Keycloak system DB) and `packages/apps/postgres/templates/db.yaml` (postgres app — backup **and** bootstrap-recovery `ObjectStore`s), via a shared `cozy-lib` helper (`cozy-lib.barman.checksumSidecarConfiguration`); - **Go-constructed** — the platform-managed `useSystemBucket=true` `ObjectStore` SSA-applied by `internal/backupcontroller/cnpgstrategy_controller.go` (`barmanSidecarConfiguration`), whose default system bucket is SeaweedFS. Regression coverage: a Go unit test on the SSA-applied `ObjectStore`, plus helm-unittest assertions on `spec.instanceSidecarConfiguration.env` for all three rendered `ObjectStore`s (mutation-checked — reverting the change fails them). Mirrors the same fix already applied to the etcd path in `cozystack/etcd-operator#342` (released in `v0.5.3`) for the same Ceph RGW backend. **Verification:** reproduced end-to-end against a real Ceph RGW endpoint. With default botocore, `PutObject` fails with the `InvalidArgument` above; with `AWS_REQUEST_CHECKSUM_CALCULATION=when_required` in the environment it succeeds. Deployed the plugin + a throwaway CNPG cluster whose `ObjectStore` carried this env: continuous WAL archiving reported `ContinuousArchiving=True` and an on-demand `Backup` completed, with the base backup and WAL segments present in the bucket. `helm template` renders the env on all three `ObjectStore`s; the `packages/apps/postgres` unit-test suite still passes. ### Screenshots N/A — no UI changes. ### Downstream repositories <!-- Walked the trigger map in docs/agents/contributing.md against the diff, file by file. The diff touches only two Helm templates (ObjectStore renders); it adds no package, no values.yaml / values.schema.json field, no API/CRD change, no variant/component/asset change, no ApplicationDefinition change, and no developer-tooling change. None of the trigger-map entries match. --> - [x] No downstream repository is affected by this change - [ ] [cozystack/website](https://github.com/cozystack/website) - follow-up: - [ ] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: - [ ] [cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack) - follow-up: - [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up: - [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up: - [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) - follow-up: - [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) - follow-up: - [ ] [cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server) - follow-up: - [ ] [cozystack/external-apps-example](https://github.com/cozystack/external-apps-example) - follow-up: - [ ] [cozystack/examples](https://github.com/cozystack/examples) - follow-up: ### Release note ```release-note fix(backups): CNPG barman-cloud backups now work against non-AWS S3-compatible object stores (Ceph RADOS Gateway, some MinIO/Cloudflare R2). The backup sidecar requests an S3 request checksum only when required, avoiding the "x-amz-content-sha256 must be UNSIGNED-PAYLOAD, ..." error that previously broke every backup and WAL-archive upload to those backends. No change for AWS S3. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved compatibility with S3-compatible, non-AWS storage backends for PostgreSQL and Keycloak backups and recovery. * Pin the barman-cloud sidecar to compute AWS request checksums only when required (`AWS_REQUEST_CHECKSUM_CALCULATION=when_required`) to prevent failures with strict S3 gateways. * **Tests** * Updated/added assertions in controller and Helm/YAML suites to verify the checksum pin is applied to generated and recovery ObjectStores (Postgres and Keycloak). <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Summary
The snapshot-agent fails to upload etcd snapshots to non-AWS, S3-compatible
object stores (Ceph RADOS Gateway confirmed; likely some MinIO / Cloudflare R2
versions too). This makes the agent compute an S3 request checksum only when
the operation requires one, on both SDK layers the upload touches — the
s3.Clientand the transfer manager — which restores uploads on those backendswhile remaining fully compatible with real AWS S3.
Root cause
Since early 2025,
aws-sdk-go-v2changed the defaultRequestChecksumCalculationfrom
WhenRequiredtoWhenSupported. WithWhenSupportedthe SDK attaches aflexible checksum (CRC32 by default) to every upload. Over HTTPS a multipart part
rides this as
x-amz-content-sha256: STREAMING-UNSIGNED-PAYLOAD-TRAILER+x-amz-trailer: x-amz-checksum-crc32. AWS S3 accepts it; Ceph RGW rejects it:The etcd snapshot itself is read fine (
opened snapshot stream→completed snapshot read); only the S3 upload fails, leaving theEtcdSnapshotwithouta stored object.
Crucially, the agent does not issue a plain
PutObjectfor a real snapshot.internal/agent/snapshot.gostreams throughmanager.Uploader, which multipartsany body over its 5 MiB part size — i.e. every real etcd snapshot. And
manager.Uploadercarries its ownRequestChecksumCalculation(hardcoded toWhenSupportedinNewUploader) that it never derives from thes3.Clientoption. So setting the option on the client alone fixes only the degenerate
sub-5-MiB single-part case and leaves real backups broken. (Thanks to Timofei Larkin (@lllamnyp)
for catching this in review — the first revision of this PR had exactly that gap.)
Fix
Derive one package-level constant
snapshotChecksumCalculation = WhenRequiredand apply it in both places that carry the knob:
destination.s3Client(agent.go) — the client option, covering thesingle-part
PutObjectandHeadObject;newSnapshotUploader(snapshot.go) — the transfer manager option, coveringthe multipart parts.
WhenRequiredcomputes a checksum only for operations that mandate one, so neithera
PutObjectnor a multipartUploadPartcarries a flexible checksum — acceptedby both AWS S3 and the affected backends. The response side (
ResponseChecksumValidation)is set symmetrically on the shared client for the restore/download path.
Impact / compatibility
including multipart, now succeed — the bug being fixed.
PutObject/UploadPartdoes not require achecksum, so nothing is added.
fixed internal default; the agent's env is a closed controller-built list, so no
operator could set
AWS_REQUEST_CHECKSUM_CALCULATIONon the Pod anyway.Integrity trade-off (stated plainly)
With
WhenSupportedthe backend validated each part's CRC32 and would reject acorrupted part. After this change there is no server-side verification of the
uploaded bytes.
uploadStreamHasheddoes tee the body through sha256, but thatdigest is over what the agent sent — never compared server-side, and not
re-checked on restore (
restore.gosetsSkipHashCheck, since a streamedsnapshot carries no etcd hash footer). Concretely, over a plain
http://path abyte-flipping proxy would today produce
BadDigest; after this it stores corruptand only surfaces at restore. The alternative is backups that don't work at all on
RGW, so this doesn't block the change — but a good follow-up is to verify the
recorded sha256 against the downloaded object in
downloadS3, restoring anend-to-end guard independent of any SDK checksum setting. (Follow-up, not folded
into this PR.)
Verification
TestUploadS3StreamMultipartNoChecksumTrailer(
snapshot_test.go): drives the realuploadS3Streamagainst a TLShttptestserver (trusted viaAWS_CA_BUNDLE, sos3Client+newSnapshotUploaderrun exactly as in production) with a 12 MiB / three-partbody, and asserts no request carries
STREAMING-UNSIGNED-PAYLOAD-TRAILER,x-amz-trailer, orx-amz-sdk-checksum-algorithm. It fails on the pre-fixuploader (reproducing the RGW trailer trace) and passes with the fix.
TestS3ClientChecksumWhenRequiredkept as a client-option unit check only(now hermetic; also asserts the response-side setting).
go build ./...,go vet ./...,go test ./...all green.Note on the earlier "verified on RGW" claim: the manual canary that succeeded
uploaded an 86 KB object — under the 5 MiB part size, so it took the single-part
path where the client option does apply. That's consistent with the analysis
above, and is why it didn't expose the multipart gap. The multipart test is now
the authoritative guard.
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Documentation
InvalidArgument: x-amz-content-sha256error seen with some S3 backends and older agent images.Tests