Conversation
NitinKumar004
left a comment
There was a problem hiding this comment.
Review notes
Real data-plane engine (per #427): N/A — OCI Object Storage is object storage, which is explicitly NOT engine-eligible under the #427 model (ComputeEngine/DatabaseEngine/CacheEngine/FunctionEngine/ContainerEngine).
Findings
High · wire-fidelity — GetNamespaceMetadata (GET /n/{ns}) is misrouted to ListBuckets when the tenancy namespace begins with 'b'
server/oci/objectstorage/handler.go:287
If a user's tenancy hashes to a namespace starting with 'b' -> every GetNamespaceMetadata call (GET /n/{ns}) returns 400 'compartmentId is required' instead of the metadata body -> S3/Swift-compat setup and any SDK flow that reads namespace metadata breaks non-deterministically for ~6% of tenancies, while the identical code works for everyone else. Fix: carry a 'hadBucketSegment' bool out of parseNamespaced instead of re-sniffing the raw path.
serveBucketCollection distinguishes /n/{ns} from /n/{ns}/b with strings.Contains(r.URL.Path, "/"+segBuckets) (segBuckets=="b"). The parsed route already discards whether a /b segment was present, so this substring heuristic is the only signal. For a namespace whose first char is 'b', the path "/n/b25a97828eed" contains the substring "/b", so the request falls through to the GET branch = listBuckets, which requires compartmentId. Reproduced with tenancy "ocid1.tenancy.oc1..probe0100" -> namespace
Medium · docs — docs/services.md not updated with OCI Object Storage (Definition-of-done item)
docs/services.md:59
If a reader consults docs/services.md to learn which OCI services cloudemu emulates -> Object Storage is invisible there even though it is fully implemented -> the service is undiscoverable via the canonical human-facing doc, and the PR fails a stated done-criterion (a future migration-drift CI check keys off this file).
docs/oci-conventions.md Definition of done requires 'Operations added to docs/services.md'. The storage section header (line 59) still reads 'AWS: S3 | Azure: Blob Storage | GCP: GCS' with no 'OCI: ObjectStorage' entry, unlike networking (line 407 'OCI: VCN ...'), OCI Monitoring (623) and Identity (670) which each have OCI subsections. Only the generated docs/coverage/* files were updated in the diff; the hand-maintained services.md was not touched.
Medium · coverage — Changed packages below the 90% pillar; lifecycle-expiry and versioning wrappers untested
providers/oci/objectstorage/retention.go:366
If EvaluateLifecycle mis-evaluates expiry (e.g. off-by-one on the ExpirationDays*24h window, or a prefix mismatch) -> a caller relying on lifecycle-expiry reporting gets wrong object lists -> the regression ships silently because no test exercises the aged-out branch. Add a FakeClock test that advances past ExpirationDays and asserts the expired name, plus a HeadObjectVersion and portable-versioning round-trip.
go test -cover: providers/oci/objectstorage 66.2%, server/oci/objectstorage 71.6% (pillar target 90%). 0%-covered flows include EvaluateLifecycle + objectExpired (retention.go:366/399 — object age-out evaluation is never asserted by any test), HeadObjectVersion (versioning.go:228), and the portable SetBucketVersioning/GetBucketVersioning wrappers (versioning.go:140/159). TestBucketLifecycleCRUD stores and reads a policy but never drives an object past its ExpirationDays to confirm EvaluateLifecy
Low · wire-fidelity — ListObjects default page size is 100, not OCI's 1000
server/oci/objectstorage/object.go:233
If a client lists a bucket of 500 objects without an explicit limit -> it receives 100 + a nextStartWith cursor and must paginate 5x -> extra round-trips and a subtle behavioral difference from real OCI. Minor and partly a shared-ocirest.DefaultLimit choice; note only.
listOptions sets MaxKeys: ocirest.Limit(r), which returns ocirest.DefaultLimit (100) when 'limit' is absent and is always >=1, so the provider's defaultListLimit (1000, matching real OCI) at object.go:15 is never reached from the wire path. Real OCI ListObjects returns up to 1000 per page by default.
Low · coverage — No oci-go-sdk SDK-compat test
server/oci/objectstorage/handler_test.go:1
If the handler's wire shape drifts from what the real SDK emits/expects (header casing, envelope fields) -> hand-rolled tests that mirror the handler's own assumptions won't catch it -> a real SDK user hits the mismatch first. Recommended, not blocking.
Convention: 'An SDK-compat test using github.com/oracle/oci-go-sdk against httptest.NewServer is the strongest evidence the handler is right. Add one where the SDK makes it practical.' All wire tests are hand-rolled httptest requests; no oracle/oci-go-sdk client is driven against the handler.
b85c29f to
f285446
Compare
|
Rebased onto current High —
|
NitinKumar004
left a comment
There was a problem hiding this comment.
Review — OCI Object Storage
Verdict: Approve (no blockers). This is the largest and highest-risk surface here (object bytes + multipart + PARs), and it's exemplary — it reuses the portable storagedriver.Bucket interface exactly as S3/GCS/Azure do, stores object bytes in-memory the way S3 does (no external engine required; optional engine offload), and follows the existing ocirest + server/oci/* conventions precisely.
Non-blocking
- Enabling versioning on a bucket that already has objects drops the pre-existing object —
providers/oci/objectstorage/versioning.go:18-41(setVersioningLocked/storeObjectLocked). Reproduced: create unversioned bucket → PUT object → enable versioning → PUT overwrite →objectversionslists only the new version; the original is neither anullversion nor recoverable. Real OCI retains the pre-existing object as thenullversion and preserves it on the next overwrite. The common IaC path (versioning = "Enabled"declared at bucket create) works perfectly — this only bites a create-then-enable sequence. - Object ETag is content-addressed (sha256 of bytes) —
providers/oci/objectstorage/objectstorage.go:327(objectETag). Real OCI mints a fresh opaque ETag per PUT; here an identical re-PUT yields the same ETag, and two distinct objects with identical content share one. A client using If-Match/If-None-Match on ETag could see a stale-looking match after an identical re-upload. - Generic OCI error codes from the shared codec —
server/wire/ocirest/ocirest.go:75-98(statusFor). Bucket-not-empty →409 IncorrectStateand duplicate create →409 Conflict, where real OCI usesBucketNotEmpty/BucketAlreadyExists. Pre-existing shared behavior (not introduced here); noting because it now affects object-storage fidelity for a client branching on the exact code string. CompleteMultipartUploadassembles the whole object via repeated append under the global write lock —providers/oci/objectstorage/multipart.go:155-164. A multi-GB commit does a large memcpy while holdingm.mu, briefly blocking unrelated buckets. Bounded by uploaded data and consistent with the cohesive-mock single-mutex design; acceptable.
Verified good
Full lifecycle over the wire: get-namespace → create bucket → PUT object bytes → GET/HEAD bytes match with stable ETag/opc-content-md5 across GET/HEAD/List → multipart create/upload-parts/commit assembles in ascending part order → abort invalidates the upload → PAR create + redeem returns bytes, read-only PAR write → 403, GET PAR omits accessUri → versioning enable + overwrite lists versions with delete markers (newest-first, isLatest) → delete → 204 → GET-after-delete → 404 → delete non-empty bucket → 409. Computed IDs/namespace/etag/timeCreated stable across reads; Snapshottable present and auto-discovered (persistence round-trips objects + versions + PAR tokens); object size / part count / list page size all bounded; single-mutex spans check-then-set on every create (no TOCTOU race); reads deep-copy; -race clean; structure/lint/go generate and the full CI matrix all green.
NitinKumar004
left a comment
There was a problem hiding this comment.
This PR no longer merges with development, and against a running serve I hit a routing collision, PAR URLs that break after a restore, and lifecycle and conditional-request gaps that a real client would notice.
Earlier review points
- GET /n/{ns} misrouted for a namespace starting with b: fixed.
HasBucketSegis in place andTestNamespaceMetadataWithBPrefixedNamespacepasses. - docs/services.md: fixed.
- Coverage: fixed. It is now 96.0% (provider) and 95.5% (wire).
- ListObjects default page size: fixed. It is 1000 when no limit is given.
- oci-go-sdk compat test: not added. The author explained why. That is fine with me, but see the Terraform note under Checked.
- Enabling versioning after objects exist drops the original: still open. Details are under Medium.
- Content-addressed object ETag: still open.
objectETagis still sha256 of the bytes. - Generic error codes (
Conflict/IncorrectStateinstead ofBucketAlreadyExists/BucketNotEmpty): still open. - Multipart commit runs under the global lock: unchanged, and acceptable as noted before.
High
1. Does not merge with current development
- Conflicts are in
contrib/realengine/blobstore/blobstore.go, where development reworded the package doc, and indocs/coverage/README.md, which is generated. - After resolving them,
go run ./internal/coveragegenstill changesdocs/coverage/README.md, because the storage row has no OCI link. It also changesdocs/coverage/oci/objectstorage.md. - Fix: rebase on development, rerun
go generate ./...and commit the output. - On the merged tree,
go build ./...andgo vetare clean. Tests pass for providers/oci, server/oci, persist (TestSnapshotCompleteness), internal/coveragegen, serveflags, serverkit and seed.-racepasses on providers/oci and server/oci.
2. The work request handler takes over Object Storage paths (server/oci/oci.go:86, server/oci/workrequest/workrequest.go:200)
- The workrequest handler is registered first. It claims any GET whose path has a
workRequestssegment followed by at most two more segments. Object keys and bucket names are user data, so real objects become unreachable. - Repro against serve:
PUT /n/{ns}/b/tfb/o/workRequests/xreturns 200.GETon the same path returns404 {"code":"NotAuthorizedOrNotFound","message":"work request x not found"}.- Create a bucket named
workRequests, thenGET /n/{ns}/b/workRequestsreturns400 compartmentId is required.
- Fix: have the workrequest handler ignore
/n/...and/p/...paths. Or register Object Storage before it, since itsMatchesis anchored at/nand/p/{token}/n. Add a test with a key and a bucket namedworkRequests.
Medium
3. PAR tokens collide after a persist restart (providers/oci/objectstorage/par.go:123)
- The redemption token comes from
idgen.GenerateID(""), which is the process counter. On restart that counter starts again from 1 while restored PARs keep their tokens. - Repro:
- Start
serve --providers oci --persist --persist-strategy on-shutdown --state-file s.json. - Create a PAR on bucket
secret. Its access URI is/p/00000007/.... - Restart, then create a PAR on bucket
public. It also gets/p/00000007/...and the same PAR OCID. - Now
GET /p/00000007/n/{ns}/b/secret/o/payroll.csvreturns 403 on every attempt.
- Start
- This breaks the claim that an access URI issued before a snapshot still redeems after restore.
- Fix: mint the token from crypto/rand.
idgenalready has random helpers. Also check for collisions inResolvePAR, or key PARs by token.
4. Enabling versioning after objects exist loses the original object (providers/oci/objectstorage/versioning.go:18)
- Repro: PUT
k= "original", then UpdateBucket{"versioning":"Enabled"}, then PUTk= "second".GET /objectversionslists one version, and "original" is gone. setVersioningLockedonly allocates the history map. It never seeds the existing objects as versions.- Fix: when versioning moves to Enabled, add each current object to its chain before the first overwrite.
5. Lifecycle rules do not round-trip (server/oci/objectstorage/lifecycle.go:141, :129, types.go:221)
- PUT
{"timeAmount":1,"timeUnit":"YEARS"}reads back astimeAmount:365,timeUnit:"DAYS". Any client that compares the policy it set with what it reads back will see a diff. targetandobjectNameFilter.inclusionPatterns/exclusionPatternsare silently dropped. Repro: PUT a DELETE rule withtarget:"previous-object-versions"andinclusionPatterns:["*.log"]. It is accepted with 200, and GET returns a plain DELETE rule with no target and no filter, so the rule would now act on live objects.- The PR says nothing is accepted and then ignored, but that is not the case here.
- Fix: store the unit, target and all three filter lists as sent. Reject
ABORTunless the target ismultipart-uploads. Return the policy'stimeCreated.
6. Conditional and range headers are ignored (server/oci/objectstorage/object.go:56, :94)
PUTwithif-none-match: *on an existing object returns 200 and overwrites it.PUTwithif-match: bogusalso returns 200 and overwrites. Real OCI answers 412IfNoneMatchFailed/IfMatchFailed.GETwithRange: bytes=0-2returns 200 with the full body. Real OCI returns 206 withContent-Range. Ranged and parallel downloads (CLI, rclone, SDK range reads) get the wrong bytes.- Fix: honour
if-matchandif-none-matchon Put, Get, Head and Delete object and on UpdateBucket/DeleteBucket. Also support a single byte range on GetObject.
7. UpdateBucket silently ignores a rename (server/oci/objectstorage/types.go:28)
UpdateBucketDetails.namerenames the bucket in OCI. HerePOST /n/{ns}/b/tfb {"name":"tfb2"}returns 200 withname:"tfb", andGET /b/tfb2returns 404.- Fix: support the rename and move the bucket's stores under the new key. If that is out of scope, reject a
namethat differs from the current one with 400.
8. Compartment existence is not checked (server/oci/objectstorage/bucket.go:36, providers/oci/objectstorage/bucket.go:112, :195)
CreateBucketwithcompartmentId: "ocid1.compartment.oc1..doesnotexist"returns 200.- Development now rejects that for VCN through
SetCompartmentChecker(#1295). Real OCI answers 404NotAuthorizedOrNotFound. - Fix: wire the same identity-backed checker in
server/oci/oci.gofor create and for thecompartmentIdmove on update.
9. PAR rules are stricter than OCI (providers/oci/objectstorage/par.go:16, :144, :96)
- A
timeExpiresthree months out is rejected withexceeds the maximum lifetime of 168h0m0s. Seven days is the S3 presign limit. OCI PARs commonly live for months or years, for example a Terraformtime_expiresa year out. AnyObjectReadwithobjectName:"logs/"is rejected. OCI treatsobjectNameon the AnyObject types as a prefix.- Fix: drop the cap or raise it far enough not to reject realistic values, and support prefix-scoped bucket PARs in
PARAllows.
Low
- Error codes (
server/wire/ocirest/ocirest.go:75): a duplicate bucket returns409 Conflictand a non-empty bucket returns409 IncorrectState. OCI usesBucketAlreadyExistsandBucketNotEmpty. Map these in the Object Storage handler rather than in the shared codec. - ETag is a content hash (
providers/oci/objectstorage/objectstorage.go:327): two identical PUTs return the same ETag. Once item 6 lands,if-matchwill treat a re-upload as unchanged. Mint an opaque ETag per write. - Pagination is missing: ListBuckets, ListObjectVersions, ListPreauthenticatedRequests and ListMultipartUploads ignore
limitandpageand never setopc-next-page.objectversions?limit=1returned all 3 versions. - copyObject drops fields (
server/oci/objectstorage/object.go:323):destinationRegionis required by OCI but not checked.eu-frankfurt-1was copied locally with 202.destinationObjectMetadata,destinationObjectStorageTierand the if-match fields are dropped. Reject a foreign region and apply or reject the other fields. - Multipart commit does not check ETags (
server/oci/objectstorage/multipart.go:131):partsToCommitwith"etag":"wrong-etag"commits successfully. Compare each part's ETag. - Bucket names are not validated:
"bad name!"is accepted. OCI allows letters, digits,-,_and., up to 256 characters.
Checked
- Real-user flow on
serve --providers ociover raw HTTP:- Bucket create, then GET twice (byte-identical), update, list and delete.
- Object put, get and head (Content-Length and type correct).
- Version list and delete markers.
- Async copy returns 202, and
/workRequests/{id}reports SUCCEEDED. - Multipart create, upload and commit.
- PAR redeem works, write through a read PAR returns 403, and redeem after revoke returns 404.
- Wrong namespace returns 404, and deleting a missing object returns 404.
- Terraform with oracle/oci was not run. The provider has no per-service endpoint override I know of, so it cannot reach a local port without DNS and TLS interception.
- Wiring: provider factory,
DriversFrom, monitoring wiring, resource discovery and the seed target all pick up Object Storage. - Snapshot:
Snapshottableis discovered by the persist completeness test. - Architecture: behaviour lives in the provider and the wire layer delegates, so the library and serve behave the same.
- Lint and format: golangci-lint is clean on the new packages, and gofmt is clean.
--enforce-auth: there is no OCI authorization gate on development, so there is nothing to declare for this handler.
|
Please merge the latest |
Implements OCI Object Storage against the portable storage driver.
providers/oci/objectstorage holds the Mock over memstore.Store, satisfying
driver.Bucket and the optional driver.VersionedBucket. Buckets carry OCI's
settings (public access type, storage tier, versioning tri-state, KMS key,
auto-tiering) and record the compartment they were created in. Objects carry
opc-meta- user metadata, a content MD5 and a per-object storage tier.
Retention rules hold objects against delete and overwrite, and a locked rule
can only be extended. A pre-authenticated request is a first-class resource
with its own OCID, lifetime and redemption token, so GeneratePresignedURL
creates a real PAR that ListPARs and DeletePAR can see and revoke.
server/oci/objectstorage serves the /n/{namespace}/b/{bucket}/o/{object}
surface, plus multipart uploads, object versions, retention rules, the
lifecycle policy, PAR management and PAR redemption at /p/{token}/n/…. The
OCI-only behaviour is a consumer-side Extras interface satisfied by the mock;
a driver that does not satisfy it is served 501. ListBuckets requires
compartmentId; the bucket-scoped lists take what real OCI takes. copyObject
is asynchronous and records a work request, as real OCI does.
Operations with no OCI equivalent are named rather than silently accepted:
bucket policies (Identity policies do that), CORS, object tags (objects carry
opc-meta- metadata), reencrypt and restoreObjects, and multipart
partsToExclude.
… add persistence
Review follow-ups on the OCI Object Storage PR.
GetNamespaceMetadata misrouted to ListBuckets whenever the tenancy namespace
began with "b": serveBucketCollection separated /n/{ns} from /n/{ns}/b by
sniffing the raw path for the substring "/b", so /n/b25a97828eed matched and
fell through to listBuckets, which 400s without compartmentId. parseNamespaced
now carries whether a /b segment was actually parsed.
HeadObject reported neither Content-Length nor the object's Content-Type: a HEAD
carries no body, so a client had no way to learn the size. It now answers
through its own writer rather than the JSON helper, whose application/json was
overwriting the object's type.
ListObjects applied ocirest.DefaultLimit (100) when the caller named no limit,
so the provider's own 1000 — real OCI's page size — was unreachable from the
wire. An absent limit is now left for the provider to fill in, leaving
DefaultLimit alone for the other OCI services.
Object bytes now flow through config.WithStorageEngine, the seam AWS S3, Azure
Blob and GCP GCS already use, keyed by object version so each version is
addressed separately. Object and version records track Size independently, so
Head, List and a bucket's approximate size stay correct once the bytes are
offloaded.
Adds Snapshottable, which persist's completeness guard (#582) now requires:
buckets, objects, version chains, PARs (with their redemption tokens, so an
access URI issued before a snapshot still redeems), retention rules and the
lifecycle policy round-trip under their original identities.
Object Storage metrics were silently dropped: the provider publishes to
oci_objectstorage, and the OCI Monitoring mock refused any Oracle-reserved
namespace on every path. The reservation now applies to PostMetricData, the
customer-facing one, and not to the seam Oracle's own emulated services use.
Adds --oci-tenancy so the tenancy — and therefore the Object Storage namespace
derived from it — is reachable from the CLI, as the AWS account, Azure
subscription and GCP project already are.
Documents the service in docs/services.md, and raises coverage to 93.9%
(provider) and 94.9% (wire) from 66.2% and 71.6%.
…ens, OCI-faithful wire semantics
The work request poller is registered ahead of every OCI service and claimed
any GET with a workRequests segment followed by at most two more, so an object
key or a bucket named workRequests was unreachable. workRequests is now only
recognized directly after a date-style API version, or leading the path for
Object Storage's unversioned /workRequests/{id}; /n/… and /p/… never match.
PAR redemption tokens came from the process id counter, which restarts after a
persist restore while restored PARs keep theirs, so a new PAR could take an old
one's token and OCID and lock the old URL out. Tokens now come from crypto/rand,
and both the token and the OCID are checked for collisions. timeExpires is
required with no S3-style week cap, and on the AnyObject types objectName is a
prefix.
Enabling versioning now seeds the objects already present as versions, moving
their engine bytes to the versioned reference, so the first overwrite keeps
the original.
Lifecycle policies are stored OCI-natively and read back exactly as written:
unit, target and all three objectNameFilter lists, plus timeCreated. ABORT
requires the multipart-uploads target; only rules aimed at objects age out
live objects; patterns are honored. The portable shape refuses a rule it
cannot express instead of flattening it.
ETags are opaque and minted per write. if-match / if-none-match apply to put,
get, head and delete object, to UpdateBucket and DeleteBucket, and to both
sides of copyObject; GetObject serves a single byte Range with 206.
UpdateBucket's name renames the bucket, its objects, versions and PARs moving
with it. A bucket created in, or moved to, a compartment Identity does not know
is 404, wired the same way as VCN.
Lows: BucketAlreadyExists / BucketNotEmpty codes in the Object Storage handler;
limit/page pagination with opc-next-page on every list; copyObject requires a
local destinationRegion and applies the source version, both ETag
preconditions, metadata and storage tier; multipart commit checks part etags;
bucket names are validated.
Takes providers/oci/monitoring verbatim from the Notifications branch, so the two OCI PRs carry an identical change there and whichever merges second resolves without a conflict. monitoring.go is back to development's version and PutMetricData again refuses oci_ namespaces from portable callers; the only way into a reserved namespace is PostServiceMetricData. Object Storage now emits through that path via a private serviceMetricSink, into the bucket's own compartment and keyed by resourceId, instead of widening PutMetricData. TestMetricsLandInTheBucketCompartment covers it and fails with the bucket-compartment lookup removed.
f285446 to
329035e
Compare
|
Rebased onto High1. Rebase. 2. Work requests taking over Object Storage paths. Fixed at the root in One deviation from a pure version-anchored rule: I kept the unversioned
Medium3. PAR tokens after a persist restart. Tokens are now 48 bytes from Worth flagging beyond this PR: every OCI service mints OCIDs from that process counter, and persist never advances it. That is the underlying cause, and it can affect any OCI service across a restore. I've fixed the PAR case here, but the general fix belongs in a separate change. 4. Versioning after objects exist. Turning versioning on now records every existing object as a version, moving its engine bytes to the versioned key. Covered for all three ways of enabling it, with and without an engine. 5. Lifecycle round-trip. The provider stores the OCI policy itself rather than the portable shape: unit, target, all three filter lists and 6. Conditional and Range headers. Preconditions are honoured everywhere you listed, plus both sides of One deviation: a matching 7. UpdateBucket rename. Implemented: objects, versions, PARs and engine bytes move to the new name. A taken name returns 8. Compartment check. Mirrors development's VCN pattern exactly, wired from 9. PAR rules. The week cap is removed, so a PAR a year out is accepted. Lows (all done)
Aligning with #421 (
|
NitinKumar004
left a comment
There was a problem hiding this comment.
Every finding from the last round is fixed, and I confirmed each one against serve with the real oci-go-sdk (v65.126.1). Two new problems turned up in the same areas. Restored versions and retention rules can still be overwritten after a restart, and copy work requests report a status the Object Storage SDK does not define.
Earlier findings
- High 1, merge with development: fixed. The branch is based on the current tip of development (770204c), so it merges cleanly.
go run ./internal/coveragegenleaves no diff indocs/coverage. - High 2, work requests taking over Object Storage paths: fixed. PUT and GET of
tfb/o/workRequests/xreturnpayload. A bucket namedworkRequestscan be created, read, written to and listed. Copy work requests still answer on/workRequests/{id}throughGetWorkRequest, andListWorkRequestsworks too. - Medium 3, PAR tokens after a persist restart: fixed. I ran the original repro: start with
--persist --persist-strategy on-shutdown, create a PAR onsecret, send SIGINT, restart, then create a PAR onpublic. The two PARs got different OCIDs and different tokens, and thesecretURI still returnssalaries. - Medium 4, enabling versioning after objects exist: fixed. The sequence PUT
original, enable versioning, PUTsecondnow lists 2 versions, and both bodies come back by versionId. Suspending, writing, re-enabling and writing again keeps all 5 versions. - Medium 5, lifecycle round-trip: fixed. A rule set as
1 YEARSwith targetprevious-object-versionsand all three filter lists reads back exactly as sent, andtimeCreatedis set. ABORT on theobjectstarget returns 400. - Medium 6, conditional and range headers: fixed. PUT with
if-none-match: *returns 412IfNoneMatchFailed. A bogusif-matchreturns 412IfMatchFailedon PUT, GET, DELETE and UpdateBucket. A matchingif-none-matchon GET and HEAD returns 304. Rangesbytes=0-2,bytes=-3andbytes=6-return 206 with the right bytes andContent-Range.bytes=50-60returns 416. - Medium 7, UpdateBucket rename: fixed.
rbrenamed torb2keeps its objects, the old name returns 404, and renaming onto a taken name returns 409BucketAlreadyExists. - Medium 8, compartment check: fixed. CreateBucket and a compartment move both return 404
NotAuthorizedOrNotFoundfor a compartment that does not exist. - Medium 9, PAR rules: fixed. A PAR that expires a year out is accepted. With
objectName: "logs/"on AnyObjectRead,logs/a.txtredeems andother/b.txtreturns 403. A missingtimeExpiresreturns 400. - Low, error codes: fixed. Duplicate bucket returns
409 BucketAlreadyExists. A non-empty delete returns409 BucketNotEmpty. - Low, ETag: fixed. Two identical PUTs now return different ETags.
- Low, pagination: fixed.
objectversions?limit=1returns 1 item plusopc-next-page, and page 2 returns a different version. Paging ListBuckets withlimit=2returns the same total as one unpaged call. - Low, copyObject: fixed.
destinationRegion: eu-frankfurt-1returns 400. - Low, multipart ETags: fixed.
wrong-etagis rejected with 400, and the real part ETag commits. - Low, bucket names: fixed.
bad name!returns 400.
New findings
Medium 1. Version ids and retention rule OCIDs still collide after a persist restore (providers/oci/objectstorage/versioning.go:15, providers/oci/objectstorage/retention.go:82)
This is the same root cause as the PAR token issue. The PAR fix did not cover the other ids this service mints. newVersionID and the retention rule OCID both come from the process counter, which starts again from 1 on restart.
Repro for versions:
- Start
serve --providers oci --persist --persist-strategy on-shutdown. - Create bucket
vbwith versioning Enabled and PUTksix times. - Send SIGINT and restart, then PUT
ktwelve more times. GET /objectversionsreturns 18 items but only 13 distinct ids.0000000f,0000000d,0000000b,00000009and00000007each appear twice.GET /o/k?versionId=0000000freturnsbefore-6. The newer version with the same id cannot be read, and a delete by that versionId is ambiguous. With a storage engine wired,engineRef(bucket, key, version)is the same for both versions, so the new write replaces the old version's bytes.
Repro for retention rules:
- Before the restart, create three 365-day rules on
rb. They get the OCIDs...05,...07and...09. - After the restart, create six 1-day rules.
- The list now shows six rules, all 1-day. The new rules reused
...05,...07and...09, andbkt.retention.Set(rule.ID, rule)silently replaced the original 365-day rules. A rule withtimeRuleLockedset can be replaced the same way.
Fix: mint version ids with idgen.UUID(), since real OCI version ids are opaque UUIDs. Give retention rule OCIDs a random suffix as well, or refuse a Set that would replace an existing id. Bucket OCIDs collide the same way: after the restart a new bucket got ...12, the same OCID as vb. You already called that out as the general OCI counter problem, so a tracked follow-up is fine for buckets. Versions and retention rules lose data here, so they should be fixed in this PR. Add a restore test for both, like TestPARsDoNotCollideAfterRestore.
Medium 2. Copy work requests report SUCCEEDED, which is not an Object Storage status (server/oci/objectstorage/object.go:482, server/oci/workrequest/workrequest.go:93)
The Object Storage WorkRequestStatusEnum is ACCEPTED, IN_PROGRESS, FAILED, COMPLETED, CANCELING and CANCELED. The shared store always writes SUCCEEDED, which is the right value for VCN and Identity but not for Object Storage.
Repro: run CopyObject, then GetWorkRequest on the returned opc-work-request-id. The status is SUCCEEDED, and objectstorage.GetMappingWorkRequestStatusEnum("SUCCEEDED") returns false. A copy waiter that polls for COMPLETED never finishes. That includes the Terraform object copy and oci os object copy --wait-for-state COMPLETED.
Fix: let Accept take the terminal status, or add an Object Storage variant, and record COMPLETED for COPY_OBJECT. Add an SDK-shaped test that checks the status against the Object Storage enum.
Checked
- The branch merges cleanly with origin/development, and coveragegen leaves no diff.
go build ./...is clean.go vetis clean on providers/oci, server/oci, services/storage, serveflags, persist and internal/coveragegen.- Tests pass on those packages and on contrib/realengine/blobstore.
-racepasses on providers/oci and server/oci. golangci-lint --new-from-rev=origin/developmentreports 0 issues.- The
metrics.gohunks match #421 line for line, so whichever PR merges second will not conflict there. - Every earlier repro was run against
servewith oci-go-sdk v65, including a real SIGINT restart for the persist cases. - Terraform was not run, for the same reason as before. The oracle/oci provider has no per-service endpoint override.
Summary
storagedriver.services/storage/driver— OCI-only behaviour is a consumer-sideExtrasinterface, per the rule set in Move OCI-only capabilities out of shared driver packages #393.Closes #410. Part of #376.
Changes
providers/oci/objectstorage/—Mockovermemstoreimplementingdriver.Bucket, guarded by async.RWMutex: namespace derivation, buckets, objects, versioning, multipart, retention, PARs.server/oci/objectstorage/— the/n/{namespace}/b/{bucket}/o/{object}surface plus PAR redemption at/p/{token}/n/….providers/oci/oci.goandserver/oci/oci.go.Operations
Namespace + metadata; bucket create/get/head/update/delete/list; object put/get/head/delete/list (prefix, delimiter, paging); rename and
updateObjectStorageTier; asynccopyObject; multipart create/upload/list-parts/commit/abort/list; object versions (Disabled/Enabled/Suspended, version-addressable GET/HEAD/DELETE, delete markers,objectversions); retention rules with lock semantics; lifecycle policy PUT/GET/DELETE; PARs including redemption.PARs are modelled as real resources
A PAR gets its own OCID,
timeExpires, an opaque redemption token, and is listable and revocable — not a fabricated signature.GeneratePresignedURLcreates one, so the URL it returns actually works and can be revoked. Demonstrated in the transcript: read-only PAR servesGET, refusesPUTwith 403, and 404s after revocation.Compartment scoping — one deliberate narrowing
Only
ListBucketscallsRequireCompartmentID; it is the one collection real OCI scopes by compartment. Object, PAR, retention and upload lists are bucket-scoped and take nocompartmentId, so requiring it would reject calls real OCI accepts.CreateBucketrequires it in the body. Stated in the package doc.Never accept-and-ignore
Rejected by name rather than silently dropped: bucket policies (→ Identity policies), CORS, object tags (→
opc-meta-),reencrypt,restoreObjects, multipartpartsToExclude, cross-namespace copy, more than one lifecycleinclusionPrefix, disabling encryption, versioning back toDisabled, and unknown access types / tiers / time units.reencryptis a 501 rather than a 202: with no per-object key material there is nothing to re-wrap, and a 202 there would be theatre.Provider Coverage
Checklist
go test ./...) — exit 0, 272 packagesgolangci-lint run --timeout=9m) — 0 issuescloudemu_test.go— driver + handler tests insteadTest Plan
Coverage leak check clean: no OCI operation appears in
docs/coverage/{aws,azure,gcp}/*.md, andgit diff development -- services/is empty.End-to-end on a running server (port 4611):
Note on the suite
Other Wave 2 branches see a pre-existing failure in
cmd/cloudemu TestServeOutOfProcess("AWS endpoint never became ready") which reproduces on cleandevelopment— it spawns a child process that cannot bind a listener in a sandbox. It passed on this run; flagging it as flaky-environmental rather than related to this change.