test: replace the MinIO container with an S3 server on Bun.serve

Merged
RO
1mo ago
+18381-111

Problem

  • test/js/bun/s3/s3.test.ts fails 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 on Bun.serve with no dependencies and the data in memory. It verifies each signature and answers like Amazon S3.
  • s3.test.ts starts it as a child process with the region us-east-1. The minio service is gone from the docker files and harness.ts.
  • Verified: s3.test.ts and test/packages/s3-server/test/ (2056 tests), with bun bd test on 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.serve removes . 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() runs cli.ts in 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 calls stop() or ends.
  • S3Server.fetch is the server as a function. A test can put its own Bun.serve in front of it to inject a fault.
  • server.requests has the operation name, the status and the error code of each request.
  • SigningClient sends signed requests for operations that Bun.S3Client has no method for.
  • Options: credentials (more than one account), region, domains, buckets (each with its region, versioning and owner), clock, tls.
  • With the region option the server refuses a signature for another region. s3.test.ts uses it, so each of its requests must sign for us-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, Upload of @aws-sdk/lib-storage with 8 checksum settings, getSignedUrl, and createPresignedPost.
  • The S3 API model of the AWS SDK gave the names of headers, parameters and elements. The Ceph s3-tests suite 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.ts passes under bun bd test with --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

WhatWhereResult
s3.test.ts, 307 tests, no R2 credentialsWindows x64, 16 vCPU, release build4.9 s
test/packages/s3-server/test/, 2056 testsWindows x64, 16 vCPU, release build3.2 s
s3.test.tsLinux x64, bun bd (debug, ASAN), machine with a high load167 s
test/packages/s3-server/test/, 11 filesLinux x64, bun bd, same machine38 s to 73 s for each file
Server CPU for the upload matrix of s3.test.ts (75 uploads, 645 MiB)Linux x64, release build1.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-Expires out of range is status 400 (s3-tests expects 403). InvalidChunkSizeError is status 403.
  • Objects: If-None-Match with an entity tag on PutObject is 501. 11 tags are BadRequest (s3-tests: InvalidTag). A partNumber above the count is 416 InvalidPartNumber (s3-tests: 400 InvalidPart). 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-Modified of the object is the time when the upload started. DeleteBucket removes a bucket that has only uploads in progress.
  • Lists: encoding-type=url writes a space as + (s3-tests: %20). An empty continuation token is InvalidArgument (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-data is 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 with PermanentRedirect has 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 PermanentRedirect and 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-stream to a Bun.serve response without a body. A release build does not. src/runtime/server/RequestContext.rs:3821 compares the address of MimeType::OTHER.value, and OTHER is a const (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 (withoutDefaultType in test/helpers.ts).
  • Request.url of Bun.serve has no . and .. segments. node:http of Bun gives the path as the client sent it.
  • test/js/bun/s3/s3.test.ts has a test that sends a request to s3.us-west-1.amazonaws.com and expects PermanentRedirect. 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.