adapter/s3: object-level admin RPCs (Phase 2b)#811
Conversation
First of four S3 object-level admin RPCs from
docs/design/2026_05_22_proposed_admin_data_browser.md §3.1.2.
Mirrors the Phase 2a admin item RPCs on the Dynamo side (gating
pattern, sentinel translation, delegation to internal helpers
the SigV4 path already exercises). Subsequent slices add
AdminPutObject, AdminGetObject, AdminListObjects in the same
branch.
## Surface
- AdminDeleteObject(ctx, principal, bucket, key) error — write
role; idempotent (missing object → nil, matching the SigV4
deleteObject path and AWS S3 semantics)
- ErrAdminObjectNotFound — for the read paths landing in
later slices; AdminDeleteObject does NOT return this since
delete is idempotent
- ErrAdminInvalidObjectKey — empty / malformed key
Reuses ErrAdminBucketNotFound / ErrAdminForbidden /
ErrAdminNotLeader / ErrAdminInvalidBucketName from the existing
S3 admin sentinel set.
## Authorization
- principal.Role.canWrite() (RoleFull only)
- isVerifiedS3Leader up-front check
- Bucket-name validation matches AdminCreateBucket's contract
## Caller audit (semantic-change rule)
adminDeleteObjectTxn is a new private helper; the SigV4
deleteObject path is unchanged. The Admin call is a pure gated
wrapper that produces the same Raft op-group (single Del on the
manifest key) followed by the same async blob-chunk cleanup
(cleanupManifestBlobsAsync). No new write paths introduced.
## Tests
Five unit tests:
- HappyPath — put via SigV4, delete via admin, verify HEAD
returns 404
- Idempotent — second delete of same key returns nil; delete
of never-existed key returns nil (matches AWS contract)
- MissingBucket — returns ErrAdminBucketNotFound
- RejectsReadOnly — returns ErrAdminForbidden and leaves
state unchanged (HEAD still 200)
- RejectsEmptyKey — returns ErrAdminInvalidObjectKey
- RejectsInvalidBucketName — returns ErrAdminInvalidBucketName
## Verification
- go test -race ./adapter/... passes (5 new tests).
- golangci-lint clean (cyclop satisfied via the
adminDeleteObjectTxn split, mirroring AdminCreateBucket's
txn-body extraction pattern).
Second of four S3 object-level admin RPCs from
docs/design/2026_05_22_proposed_admin_data_browser.md §3.1.2.
## Surface
- AdminPutObject(ctx, principal, bucket, key, body io.Reader,
contentType string) error — write role; create-or-replace
semantics
- adminS3UploadMaxBytes = 100 MiB (design §3.3.3)
- ErrAdminUploadTooLarge sentinel (HTTP bridge → 413)
body is consumed exactly once; caller owns lifecycle (admin code
does NOT Close the reader). contentType is stored verbatim on
the manifest; empty defaults to application/octet-stream.
## Implementation
adminPutObjectStream is a single-attempt (no retry) chunked
upload mirroring the SigV4 putObject pipeline:
1. load bucket meta + previous manifest
2. stream body through s3ChunkSize buffers
3. dispatch in batches of s3ChunkBatchOps chunks
4. final commit (manifest + bucket fence) in one OperationGroup
5. async cleanup of the previous manifest's blob chunks
Per-stage error path runs cleanupManifestBlobs synchronously so
a partial upload that fails mid-way does not leak orphan blobs.
On commit-success the caller fires cleanupManifestBlobsAsync
for the REPLACED manifest only.
The carrier nolint matches the existing SigV4 putObject
precedent (s3.go's `//nolint:cyclop,gocognit,gocyclo,nestif`):
the chunk pipeline is a sequential N-stage flow with per-stage
cleanup that needs the local manifest view at every stage.
## Caller audit (semantic-change rule)
adminPutObjectStream is a new private helper; SigV4 putObject is
unchanged. The admin path does NOT do the SigV4 path's
sha256 / x-amz-content-sha256 / IfMatch / chunked-transfer
preconditions — those are SigV4 wire concerns and the in-process
Go API does not need them. The dispatch + manifest commit shape
is identical to SigV4: same OperationGroup layout, same key
prefixes, same cleanup contract.
## Tests
Seven unit tests:
- HappyPath_RoundTripsViaSigV4Get — admin PUT followed by SigV4
GET; verifies body + Content-Type round-trip
- DefaultsContentType — empty content-type stored as
application/octet-stream
- ReplacesExisting — second PUT overwrites first
- MissingBucket — returns ErrAdminBucketNotFound
- RejectsReadOnly — returns ErrAdminForbidden
- RejectsEmptyKey — returns ErrAdminInvalidObjectKey
- RejectsOversizedBody — body > 100 MiB returns
ErrAdminUploadTooLarge AND no manifest is committed
- AcceptsNilBody — nil reader treated as zero-byte payload;
object is readable as 0 bytes
## Verification
- go test -race ./adapter/... passes (7 new tests including
the 100 MiB cap boundary).
- golangci-lint clean.
Third of four S3 object-level admin RPCs from
docs/design/2026_05_22_proposed_admin_data_browser.md §3.1.2.
## Surface
- AdminObject — admin-facing metadata projection (Key, Size,
ContentType, ETag, LastModified time.Time, StorageClass).
No internal-only state (uploadID, chunk sizes, ...) leaks
to admin handlers / SPA.
- AdminGetObject(ctx, principal, bucket, key) (io.ReadCloser,
AdminObject, error) — read role; caller MUST Close the body
to release the read-tracker pin.
## Implementation
s3AdminObjectReader is a lazy chunk-fetcher that walks the
manifest parts on demand. Memory bounded to ~s3ChunkSize per
in-flight chunk regardless of object size (so older objects
predating the 100 MiB admin PUT cap stream without ballooning
the leader's heap).
Lifecycle: the reader owns the pin acquired by adminLoadObjectForRead
and releases it on Close. Close is idempotent (second call is a
no-op); Read after Close returns io.ErrClosedPipe.
adminLoadObjectForRead is the per-call read-path preamble:
acquire pin, load bucket meta, load manifest, with strict
release-on-error discipline so a caller hitting any early-exit
path never has to handle pin cleanup itself.
## Caller audit (semantic-change rule)
s3AdminObjectReader is a new private type; adminLoadObjectForRead
is a new helper. SigV4 getObject + streamObjectChunks are
unchanged. The admin GET path does NOT do the SigV4 path's
Range / If-Match / Content-Range work — admin reads are full-body
only (the Web Console downloads always serve the whole object).
## Tests
Eight unit tests:
- HappyPath — admin GET after admin PUT; verifies body + all
metadata fields
- StreamsMultipleChunks — payload of 3*s3ChunkSize+17 bytes
exercises the multi-chunk reader path
- MissingObject — returns ErrAdminObjectNotFound
- MissingBucket — returns ErrAdminBucketNotFound
- AllowsReadOnly — read role suffices (regression: a future
canWrite() refactor would silently break the read-only
operator dashboard)
- DefaultsContentType — empty stored content-type renders as
application/octet-stream on the admin shape
- CloseIsIdempotent — second Close is a no-op; Read after
Close errors
- RejectsEmptyKey — returns ErrAdminInvalidObjectKey
## Verification
- go test -race ./adapter/... passes (8 new tests).
- golangci-lint clean (cyclop on AdminGetObject satisfied via
the adminLoadObjectForRead split).
Final S3 object-level admin RPC from
docs/design/2026_05_22_proposed_admin_data_browser.md §3.1.2.
## Surface
- AdminListObjectsOptions { Prefix, Delimiter, ContinuationToken,
MaxKeys [1, 1000] default 100 }
- AdminObjectListing { Objects, CommonPrefixes,
NextContinuationToken }
- AdminListObjects(ctx, principal, bucket, opts) (listing, error)
— read role; reuses the SigV4 s3ContinuationToken cursor
shape so the encoder + decoder stay in one place
- defaultS3ContentType constant extracted (3 use sites in
this file)
- clampAdminListMaxKeys helper for the [1, 1000] / default-100
folding
## Implementation
Mirrors the SigV4 listObjectsV2 scan-and-collapse pipeline:
1. decode continuation token (uses ReadTS from token if set)
2. load bucket meta at that readTS (404 if absent)
3. validate token-bucket / generation / prefix / delimiter
match the request (cursor stability)
4. pin readTS
5. scan manifest keys in s3ListPageSize batches
6. per row: prefix-filter, then either emit a CommonPrefix
(delimiter mode) or decode the manifest and emit an
AdminObject
7. on emitted >= maxKeys, encode the next cursor and stop
Carries `//nolint:cyclop,gocognit,gocyclo,nestif` matching the
SigV4 listObjectsV2 precedent — the scan-collapse-emit pipeline
is sequential with per-stage error returns; splitting further
would obscure the cursor lifecycle.
## Caller audit (semantic-change rule)
Reuses the existing `s3ContinuationToken` JSON shape and
encode/decode functions. SigV4 listObjectsV2 is unchanged. The
admin path uses the same generation-fence on tokens (mismatch
→ rejected with ErrAdminInvalidBucketName-wrapped error) so
operators cannot accidentally walk a stale cursor through a
post-recreated bucket.
## Tests
Eight unit tests:
- Empty — returns non-nil Objects: [] (JSON contract)
- FlatListing — three keys, lexically ordered
- PrefixFiltersAndCollapsesWithDelimiter — verifies both
pure-prefix (no collapsing) and delimiter (subfolders
collapse to CommonPrefixes) modes against the same fixture
- MaxKeysClamped — MaxKeys=2 paginates, MaxKeys=0 defaults to
100, MaxKeys=10000 clamps to 1000
- PagesViaContinuationToken — three pages of 2/2/1 with
cursor round-trip; concatenation matches the full set
- MissingBucket — ErrAdminBucketNotFound
- AllowsReadOnly — read role suffices (regression guard)
- RejectsAnonymous — AdminPrincipal{} (zero-role) → ErrAdminForbidden
## Phase 2b complete
This slice completes the four S3 object-level admin RPCs the
design's §3.1.2 calls for. Combined with slices 1-3 (Delete /
Put / Get) the *adapter.S3Server type now mirrors the
*adapter.DynamoDBServer's Phase-2a item-level surface:
- read: AdminListObjects, AdminGetObject
- write: AdminPutObject, AdminDeleteObject
Phase 3 (HTTP handlers + bridges + integration tests) is next.
## Verification
- go test -race ./adapter/... passes (all 28 admin object
tests across the 4 slices).
- golangci-lint clean (cyclop / gocognit / gocyclo / nestif
nolints match the SigV4 precedent at s3.go).
|
@claude review Phase 2b — S3 object-level admin RPCs. Four commits, one per method:
Mirrors Phase 2a (#805) DynamoDB pattern: gated wrappers over the same primitives the SigV4 path uses. No changes to existing SigV4 codepaths. 28 unit tests; Self-review notes are in the PR body. |
|
Claude finished @bootjp's task in 4m 0s —— View job Code Review — PR #811: adapter/s3: object-level admin RPCs (Phase 2b)
|
| Gap | Test needed |
|---|---|
store.ErrReadTSCompacted on paginated scan |
TestS3Server_AdminListObjects_ExpiredToken |
| Token bucket/generation/prefix mismatch | TestS3Server_AdminListObjects_MismatchedToken |
The existing TestS3Server_AdminListObjects_PagesViaContinuationToken is strong (3-page walk, full reconstitution), but does not cover the rejection paths.
Minor nit — truncated nolint comment on AdminListObjects:
//nolint:cyclop,gocognit,gocyclo,nestif // Sequential scan-loop withThe rationale is cut off mid-sentence (compare the full explanation on adminPutObjectStream:199). Not a functional issue.
Summary
| Severity | Finding | Location |
|---|---|---|
| Medium | Missing ErrReadTSCompacted guard on paginated scan |
s3_admin_objects.go:669 |
| Medium | Token mismatch returns ErrAdminInvalidBucketName instead of a dedicated sentinel |
s3_admin_objects.go:639 |
| Minor | Two missing test cases for the above | s3_admin_objects_test.go |
| Nit | Truncated nolint comment |
s3_admin_objects.go:607 |
The code is otherwise clean, well-structured, and consistent with the SigV4 and Phase 2a DynamoDB precedents. The two medium items are straightforward to address and the rest of the implementation is ready.
There was a problem hiding this comment.
Code Review
This pull request implements object-level admin RPCs for S3, providing AdminDeleteObject, AdminPutObject, AdminGetObject, and AdminListObjects with integrated role-based access control and leader verification. The changes include support for chunked streaming and paginated listings, accompanied by comprehensive unit tests. Review feedback identifies opportunities to improve error reporting by using a more specific error for continuation token mismatches and handling expired read timestamps in the listing operation to maintain consistency with existing SigV4 paths.
| if token != nil && (token.Bucket != bucket || token.Generation != meta.Generation || | ||
| token.Prefix != opts.Prefix || token.Delimiter != opts.Delimiter) { | ||
| return AdminObjectListing{}, errors.Wrap(ErrAdminInvalidBucketName, | ||
| "continuation token does not match request") | ||
| } |
There was a problem hiding this comment.
The use of ErrAdminInvalidBucketName for a continuation token mismatch is misleading. While it correctly maps to a 400 Bad Request at the HTTP boundary, the error message will suggest a bucket name issue rather than a pagination token issue. Consider using a more specific sentinel or a generic validation error.
| if err != nil { | ||
| return AdminObjectListing{}, errors.WithStack(err) | ||
| } |
There was a problem hiding this comment.
When a continuation token is used, the storage layer might return store.ErrReadTSCompacted if the snapshot timestamp has expired. The SigV4 path in s3.go (line 2105) explicitly handles this to return a 400 error. AdminListObjects should likely do the same to ensure consistent error reporting for expired tokens.
page, err := s.store.ScanAt(ctx, cursor, end, pageLimit, readTS)
if err != nil {
if token != nil && errors.Is(err, store.ErrReadTSCompacted) {
return AdminObjectListing{}, errors.Wrap(ErrAdminInvalidBucketName, "continuation token has expired")
}
return AdminObjectListing{}, errors.WithStack(err)
}References
- Centralize common complex logic, such as retry/backoff mechanisms, into shared helper functions to avoid code duplication and improve maintainability.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cc31e7aa83
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| page, err := s.store.ScanAt(ctx, cursor, end, pageLimit, readTS) | ||
| if err != nil { | ||
| return AdminObjectListing{}, errors.WithStack(err) |
There was a problem hiding this comment.
Translate compacted continuation snapshots to client error
Handle store.ErrReadTSCompacted explicitly in AdminListObjects when a continuation token is supplied. As written, a paginated read resumed after MVCC retention has advanced returns a generic wrapped storage error from ScanAt, so callers cannot distinguish an expired token from a true backend failure (and a future HTTP bridge will likely surface this as a 500). The existing S3 listObjectsV2 path treats the same condition as an invalid/expired continuation token, which lets clients restart listing cleanly.
Useful? React with 👍 / 👎.
…d handling (r1) Addresses claude-bot r1 + gemini r1 mediums on PR #811: ## Token-mismatch sentinel Pre-fix, AdminListObjects wrapped continuation-token mismatches in ErrAdminInvalidBucketName — semantically wrong (the bucket name is fine; the token is the problem), and errors.Is(err, ErrAdminInvalidBucketName) returns true for both the actual bucket-name-invalid path and the token-mismatch path, making them indistinguishable to callers. Fix: new ErrAdminInvalidContinuationToken sentinel. The mismatch path now wraps this instead. ## store.ErrReadTSCompacted on paginated scan Pre-fix, AdminListObjects passed store.ErrReadTSCompacted up raw from s.store.ScanAt. The SigV4 listObjectsV2 at s3.go:2105 explicitly handles this — when a continuation token's readTS has been MVCC-GC'd, return 400 InvalidArgument "continuation token has expired" rather than a 500. Fix: same translation, into the same new ErrAdminInvalidContinuationToken sentinel (wrapped with "continuation token readTS has been compacted" so operator diagnostics distinguish the case). ## Carrier comment cleanup The AdminListObjects nolint comment was truncated mid-sentence (claude-bot r1 nit). Reorganised the comment block so the prose comes BEFORE the nolint marker, matching the adminPutObjectStream layout (the second-pass gci auto-fix disturbed the original). ## Caller audit (semantic-change rule) - ErrAdminInvalidContinuationToken is a new sentinel. Only caller is AdminListObjects in this file. No external callers exist yet (Phase 3 HTTP bridge doesn't ship until next phase). - The change from ErrAdminInvalidBucketName → the new sentinel for the mismatch path NARROWS the error vocabulary. Any future caller doing errors.Is(err, ErrAdminInvalidBucketName) on AdminListObjects results will now miss the token-mismatch case — but since the previous reuse was a bug, this is the correct contract. - The ErrReadTSCompacted translation is a fail-closed enhancement: previously the bridge would render an opaque 500; now it renders a diagnosable 400. No existing caller relies on the 500 shape. ## Regression tests Two new unit tests: - TestS3Server_AdminListObjects_RejectsMismatchedToken — verifies both bucket-mismatch AND prefix-mismatch routes to ErrAdminInvalidContinuationToken - TestS3Server_AdminListObjects_RejectsExpiredToken — injects store.ErrReadTSCompacted via a wrapping scanAtErrStore on the second page's ScanAt; asserts ErrAdminInvalidContinuationToken surface The scanAtErrStore is a tiny test-only wrapper that pass-through embeds store.MVCCStore and overrides ScanAt to return a pre-set error. The pattern can be reused for other admin-error-translation tests. ## Verification - go test -race ./adapter/... passes (2 new + all existing admin tests). - golangci-lint clean.
|
Warning Review limit reached
Your plan currently allows 1 review/hour. Refill in 21 minutes and 3 seconds. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more review capacity refills, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
✨ Finishing Touches🧪 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 |
|
@claude review Round 1 fixes (commit Token-mismatch sentinel: New
Nit: Also fixed the truncated Regression tests:
Caller audit:
Re: the prior CI "failure" — that was reviewdog hitting 401 Bad Credentials trying to post inline comments; the actual lint check reported |
|
Claude finished @bootjp's task in 2m 5s —— View job Code Review — PR #811 Round 2 (commit
|
Claude-bot r2 nit on PR #811: the Sentinels list pre-dates the r1 ErrAdminInvalidContinuationToken addition. The list omits the new sentinel and the surrounding prose ("reuses ErrAdminInvalidBucketName for the only validation path") is now inaccurate. Refresh the list to: - add ErrAdminInvalidContinuationToken with both shapes it covers (field-mismatch + compacted-readTS) - drop the stale ErrAdminDynamoValidation-not-used line (vestigial from a much earlier draft; never applicable) - call out that a structurally invalid (non-base64) token still surfaces as a raw json/decode wrapped error, not the sentinel Doc-only — no runtime behavior change. go test -race ./adapter/... passes; golangci-lint clean.
|
@claude review Round 2 (commit The AdminListObjects Sentinels list now:
No runtime behavior change. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f642f496a6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| meta, exists, err := s.loadBucketMetaAt(ctx, bucket, readTS) | ||
| if err != nil { | ||
| return AdminObjectListing{}, errors.WithStack(err) | ||
| } |
There was a problem hiding this comment.
Translate compacted token failures from bucket-meta lookup
When a continuation token’s read_ts has already been compacted, AdminListObjects can fail before ScanAt: loadBucketMetaAt calls GetAt at that stale timestamp and returns store.ErrReadTSCompacted, but this branch currently wraps and returns it as a generic storage error. That means expired tokens still surface as backend failures instead of ErrAdminInvalidContinuationToken in real compaction scenarios (the current translation only handles ScanAt), so callers cannot reliably treat this as a restartable pagination error.
Useful? React with 👍 / 👎.
…eta load (Codex r2 P2)
The r1 ErrReadTSCompacted translation only guarded the ScanAt
call. But AdminListObjects loads the bucket meta FIRST via
loadBucketMetaAt -> GetAt at the token's readTS - if the token
has aged out far enough that even the bucket-meta entry is no
longer fetchable, GetAt returns ErrReadTSCompacted BEFORE the
scan loop runs.
Pre-fix the GetAt error surfaced as errors.WithStack(err) - a
generic 500 to any future bridge - making expired tokens
indistinguishable from a true backend failure on that branch.
Fix: same translation as the ScanAt branch - guard on
`token != nil && errors.Is(err, store.ErrReadTSCompacted)` and
return ErrAdminInvalidContinuationToken wrapped with the same
"continuation token readTS has been compacted" message. Now
both paths a token can reach the compaction error from end at
the same sentinel.
## Caller audit
Single caller (AdminListObjects in this file). Same wrapped
sentinel as the ScanAt branch - no new error shape, just
applied to a second branch.
## Regression test
TestS3Server_AdminListObjects_RejectsExpiredTokenAtBucketMeta
uses a new getAtErrStore wrapper (GetAt-side counterpart of
the existing scanAtErrStore) to inject ErrReadTSCompacted at
the GetAt boundary. Asserts errors.Is(err,
ErrAdminInvalidContinuationToken).
## Verification
- go test -race ./adapter/... passes (1 new test, all existing
tests still pass).
- golangci-lint clean.
|
@claude review Round 3 (commit Translate Now both compaction error paths a token can hit end at the same wrapped Regression test: Caller audit: single caller; same wrapped sentinel as the ScanAt branch — no new error shape, just applied to a second branch.
Re: the other codex r2 finding ("Translate compacted continuation snapshots to client error") — that's the same issue I addressed in r1 commit |
|
Claude finished @bootjp's task in 2m 17s —— View job Code Review — PR #811 Round 3 (commit
|
| Round | Finding | Status |
|---|---|---|
| r1 medium | Token mismatch returned ErrAdminInvalidBucketName |
Fixed (r1 commit 975d007) |
| r1 medium | Missing ErrReadTSCompacted guard on paginated ScanAt |
Fixed (r1 commit 975d007) |
| r2 nit | Stale Sentinels list in AdminListObjects docstring |
Fixed (r2 commit f642f49) |
| r3 P2 | Missing ErrReadTSCompacted guard on loadBucketMetaAt GetAt |
Fixed (r3 commit d3f5df6) |
The implementation is clean and complete. Ready to merge.

Summary
Phase 2b of
docs/design/2026_05_22_implemented_admin_data_browser.md— the S3 object-level admin RPCs that mirror the Phase 2a (PR #805) DynamoDB item-level surface. Four methods on*adapter.S3Server, each delivered as its own commit:AdminDeleteObject(ctx, principal, bucket, key) errorAdminPutObject(ctx, principal, bucket, key, body io.Reader, contentType string) errorErrAdminUploadTooLarge); chunk-pipeline matches SigV4 putObjectAdminGetObject(ctx, principal, bucket, key) (io.ReadCloser, AdminObject, error)AdminListObjects(ctx, principal, bucket, opts) (AdminObjectListing, error)s3ContinuationTokencursor shape;MaxKeys [1, 1000]default 100Total: ~1100 LOC code + ~800 LOC tests across 4 commits, no changes to existing code paths.
Authorization
principal.Role.canRead()(RoleReadOnly or RoleFull)principal.Role.canWrite()(RoleFull only)isVerifiedS3Leader(ctx)up-front check +validateS3BucketNamefor the bucket argSentinels
ErrAdminForbidden(reused from dynamodb_admin.go)ErrAdminNotLeader(reused)ErrAdminBucketNotFound(reused from s3_admin.go)ErrAdminInvalidBucketName(reused)ErrAdminObjectNotFound(new)ErrAdminInvalidObjectKey(new)ErrAdminUploadTooLarge(new)Caller audit (semantic-change rule)
No existing signatures changed. The SigV4 paths (
getObject,putObject,deleteObject,listObjectsV2) and their helpers (loadBucketMetaAt,loadObjectManifestAt,cleanupManifestBlobsAsync,encode/decodeS3ContinuationToken,s3keys.*) are unchanged. The admin RPCs are gated wrappers that produce the same RaftOperationGroupshapes as the SigV4 path.Two new private helpers:
adminDeleteObjectTxn(slice 1),adminPutObjectStream(slice 2),adminLoadObjectForRead(slice 3). All file-local.The chunk-pipeline
//nolint:cyclop,gocognit,nestifonadminPutObjectStreamand the scan-loop nolint onAdminListObjectsmirror the existing precedent on SigV4putObject/listObjectsV2(same N-stage sequential flow with per-stage cleanup-on-error).Risk
Low. The Admin methods are gated wrappers over the same primitives (
coordinator.Dispatch,store.ScanAt,store.GetAt,s3keys.*) the SigV4 path uses today. No new write codepath, no new on-disk format.Self-review (5 passes)
OperationGroupshape as SigV4 (manifest put + bucket-fence put on AdminPutObject; manifest Del on AdminDeleteObject). Cleanup-on-error during chunked upload synchronously deletes orphan blobs before returning to the caller, matching SigV4'scleanupManifestBlobsdiscipline.isVerifiedS3Leader. Reads pinreadTSfor the duration of the manifest+chunks fetch (released byReadCloser.Close); list-path pin released bydefer.retryS3MutationwrapsAdminDeleteObjectper the SigV4 precedent.AdminGetObjectmemory is bounded to ~s3ChunkSize(1 MiB) regardless of object size.AdminPutObjectchunks ats3ChunkBatchOpsper dispatch.AdminListObjectspaginates ats3ListPageSize.loadObjectManifestAtpath as SigV4. List cursors are generation-fenced (token mismatch → reject) so a re-created bucket cannot leak stale entries to a continuing walk.Test plan
go test -race -count=1 -timeout=180s ./adapter/...— passesgolangci-lint --config=.golangci.yaml run ./adapter/...— clean (the four nolint markers match the SigV4 precedents)Phase plan
_proposed_→_implemented_