test: replace the MinIO container with an S3 server on Bun.serve
Merged
RO
1mo ago+18381+18381+01234567890123456789012345678901234567890123456789-111-111-012345678901234567890123456789
Problem
test/js/bun/s3/s3.test.tsfails on each Linux x64 lane of main:Failed to start service minio,failed to resolve reference "quay.io/minio/minio:latest",401 Unauthorized(build 120898). MinIO removed its images.- 306 of its tests need that container, so macOS, Windows and Linux arm64 never ran them.
Fix
- Add
test/packages/s3-server: an S3 server onBun.servewith no dependencies and the data in memory. It verifies each signature and answers like Amazon S3. s3.test.tsstarts it as a child process with the regionus-east-1. Theminioservice is gone from the docker files andharness.ts.- Verified:
s3.test.tsandtest/packages/s3-server/test/(2056 tests), withbun bd teston Linux x64 and a release build on Windows x64. Also the AWS signature examples and 33 operations of the AWS SDK for JavaScript.
Background
- AWS Signature Version 4 is an HMAC-SHA256 over a canonical form of the request. The server builds the same form to check it.
- #44041 keeps MinIO and builds it from source in each machine image. This PR removes the service.
- Considered the server in the process of
s3.test.ts: it would share the thread with the client under test, for 1.3 GiB of uploads.
Downsides
- The 306 tests now run on each lane: 4.9 s for the file on Windows x64 (16 vCPU, release build, no R2 tests).
- The server is an emulation. The Notes list each behaviour that no reference confirms.
Bun.serveremoves.and..path segments, so a key with such a segment cannot be used.
Notes
How to use it
import { serve, spawnServer, SigningClient } from "s3-server";
await using server = serve({ buckets: ["my-bucket"] }); // in the process of the test
const client = new Bun.S3Client(server.clientOptions("my-bucket"));
serve()runs the server in the process of the test.spawnServer()runscli.tsin a child process. It has a deadline for the start, and the program sends the common requests to a second server before it reports its address. The child process stops when the caller callsstop()or ends.S3Server.fetchis the server as a function. A test can put its ownBun.servein front of it to inject a fault.server.requestshas the operation name, the status and the error code of each request.SigningClientsends signed requests for operations thatBun.S3Clienthas no method for.- Options:
credentials(more than one account),region,domains,buckets(each with its region, versioning and owner),clock,tls. - With the
regionoption the server refuses a signature for another region.s3.test.tsuses it, so each of its requests must sign forus-east-1. The 1537 requests of the file have the same outcomes with and without the option. - The server does not share its port. A second server on a port that is in use gets
EADDRINUSE.
Operations
- Service and bucket: ListBuckets, CreateBucket, HeadBucket, DeleteBucket, GetBucketLocation, ListObjects, ListObjectsV2, ListObjectVersions, ListMultipartUploads.
- Object: PutObject, GetObject, HeadObject, DeleteObject, DeleteObjects, CopyObject, PostObject, GetObjectAttributes, RestoreObject, tagging, ACL, retention, legal hold.
- Multipart: CreateMultipartUpload, UploadPart, UploadPartCopy, CompleteMultipartUpload, AbortMultipartUpload, ListParts.
- Configuration with an effect: versioning, ACL, policy, CORS, tagging, request payment, ownership controls, public access block, object lock.
- Configuration that the server only keeps: lifecycle, encryption, website, notification, logging, replication, accelerate.
- Each other operation gets the error
NotImplemented.
What was verified, and how
- The SigV4 examples of the AWS documentation, sent as HTTP requests with a fixed clock: 4 header signatures, 1 presigned URL, the chunked upload (seed and 3 chunk signatures), the signed trailer example, and the POST policy example. Each signature matches to the byte.
- CRC32, CRC32C and CRC64NVME give the standard check values for
123456789. - The AWS SDK for JavaScript 3.1141.0 ran against the server in scripts that are not in this PR: 33 operations of
@aws-sdk/client-s3,Uploadof@aws-sdk/lib-storagewith 8 checksum settings,getSignedUrl, andcreatePresignedPost. - The S3 API model of the AWS SDK gave the names of headers, parameters and elements. The Ceph
s3-testssuite gave expected status and error codes. - The tests of the package pass under
bun bd test, also with the settings of the ASAN lane (BUN_JSC_validateExceptionChecks=1,detect_leaks=1,BUN_DESTRUCT_VM_ON_EXIT=1). s3.test.tspasses underbun bd testwith--timeout=270000, the per-test timeout of the ASAN lane. With the default of 5 s, the two tests that start a debug build of Bun as a child process time out on my machine (5.5 s and 6.5 s, load average above 400).- A debug build does not run 5 of the tests: 3 compute CRC32C or CRC64NVME over 5 MiB in JavaScript, and 2 move documents of 1000 keys. The release and the ASAN lanes run them.
- Buildkite builds 120967 and 120976 (the first and the third commit of this PR) passed on each lane.
Measurements
| What | Where | Result |
|---|---|---|
s3.test.ts, 307 tests, no R2 credentials | Windows x64, 16 vCPU, release build | 4.9 s |
test/packages/s3-server/test/, 2056 tests | Windows x64, 16 vCPU, release build | 3.2 s |
s3.test.ts | Linux x64, bun bd (debug, ASAN), machine with a high load | 167 s |
test/packages/s3-server/test/, 11 files | Linux x64, bun bd, same machine | 38 s to 73 s for each file |
Server CPU for the upload matrix of s3.test.ts (75 uploads, 645 MiB) | Linux x64, release build | 1.5 s user, 1.0 s system |
test/expected-durations.json has 6.7 s (default), 20.1 s (asan) and 6.4 s (windows) for s3.test.ts before this change. The Linux machine of these measurements had a load average above 400, so its wall times are an upper limit.
Behaviour that no reference confirms
The server follows Amazon S3 where S3 and MinIO differ. For these cases the references did not agree or had nothing, and the choice is from knowledge of S3:
- Signatures: the signature check comes before the clock skew and the expiry checks.
X-Amz-Expiresout of range is status 400 (s3-testsexpects 403).InvalidChunkSizeErroris status 403. - Objects:
If-None-Matchwith an entity tag on PutObject is 501. 11 tags areBadRequest(s3-tests:InvalidTag). ApartNumberabove the count is 416InvalidPartNumber(s3-tests: 400InvalidPart). A read with the wrong SSE-C key is 403 (s3-tests: 400). - Multipart: a repeated CompleteMultipartUpload answers 200 while the object is there. The
Last-Modifiedof the object is the time when the upload started. DeleteBucket removes a bucket that has only uploads in progress. - Lists:
encoding-type=urlwrites a space as+(s3-tests:%20). An empty continuation token isInvalidArgument(s3-tests: a normal list). - Buckets and access: GetBucketPolicyStatus without a policy is 404 (
s3-tests: 200). A grant for an email address is 405. An anonymous ListBuckets is 403, and S3 sends a redirect. - PostObject: a body that is not
multipart/form-datais 412. A checksum field needs a condition in the policy (s3-tests: accepted). - Regions and CORS: a preflight request for a bucket of another region gets
PermanentRedirect. A response withPermanentRedirecthas no CORS headers. A request with a signature that the server refuses gets the CORS headers of its bucket.
Not implemented
- Signature Version 2 and Signature Version 4A.
- The default CRC64NVME checksum of an upload that has no checksum.
- The
x-amz-*query parameters of a presigned URL are part of the signature. The server does not apply them. - Lifecycle rules, replication, notifications, website hosting, MFA delete, access points, directory buckets.
- A restore from an archive storage class completes immediately.
- The server is one endpoint. A bucket in another region than the server gets
PermanentRedirectand no endpoint serves it. - A new bucket has ACLs enabled and no public access block, the default of S3 until April 2023.
Found during this work, not changed here
- A debug build of Bun adds
content-type: application/octet-streamto aBun.serveresponse without a body. A release build does not.src/runtime/server/RequestContext.rs:3821compares the address ofMimeType::OTHER.value, andOTHERis aconst(src/http_types/MimeType.rs:278), so the two addresses are equal only when the optimizer merges them. The tests of the package remove that header (withoutDefaultTypeintest/helpers.ts). Request.urlofBun.servehas no.and..segments.node:httpof Bun gives the path as the client sent it.test/js/bun/s3/s3.test.tshas a test that sends a request tos3.us-west-1.amazonaws.comand expectsPermanentRedirect. It ran for MinIO and for R2. For the new server it now uses a bucket in another region and needs no internet. For R2 it is as before.