Docs/Sandbox object storage (Beta)
#

Sandbox object storage (Beta)

Built-in emulated S3 or Google Cloud Storage in every Sandboxes v2 sandbox: buckets, client env, public signed URLs and limits.

An app that stores files (uploads, attachments, exports) can have an emulated S3 or Google Cloud Storage server in every Sandboxes v2 sandbox, instead of reaching a real bucket. You declare the provider and the buckets; the sandbox runs the server, creates the buckets, and gives your app the standard client settings for that provider.

01Declaring it

yaml
object_storage:
  provider: s3          # or gcs
  buckets: [attachments, exports]
FieldTypeRequiredMeaning
providers3 | gcsYesWhich API the server speaks. One provider per application.
bucketslist of stringsYesAt least one, each unique. Created before your first prepare command runs.

Bucket names are 3–63 lowercase letters, digits, - and ., starting and ending with a letter or digit, with no .. and not shaped like an IP address. A GCS bucket can't be named storage, upload, download or batch.

Declaring it also:

  • adds an implicit public port named object-storage. You don't declare it, you can't name a port of your own object-storage, and it counts toward the 8-port limit;
  • runs the server on port 9000 (S3), or 4443 and 4444 (GCS). A port of your own can't use those numbers;
  • reserves the service names object-storage and object-storage-front.

For a pushed image, cofactor sandbox push adds the declared provider's server to the image. You don't install anything.

02Your data in each sandbox

Each sandbox gets its own copy of the buckets. Files your seeds put there (through an action, below) are captured in the ready snapshot with the rest of the sandbox, so every claim starts with them. A test that uploads a file changes only its own sandbox, and the data is gone when the sandbox is destroyed.

03The env your app gets

Every service, prepare command and action runs with the provider's standard client variables:

ProviderVariableValue
s3AWS_ENDPOINT_URL_S3http://127.0.0.1:9000
AWS_ACCESS_KEY_IDcofactor-sandbox
AWS_SECRET_ACCESS_KEYcofactor-sandbox-secret
AWS_REGIONus-east-1
gcsSTORAGE_EMULATOR_HOSThttp://127.0.0.1:4443
GOOGLE_CLOUD_PROJECTsandbox

The credentials are fixed and the same in every sandbox.

If your app reads different names (say ATTACHMENTS_ENDPOINT), set them in your definition's env:, which also overrides any of the above. An environment's env: overrides them too; a pushed image's own ENV does not.

yaml
env:
  ATTACHMENTS_ENDPOINT: http://127.0.0.1:9000
  ATTACHMENTS_BUCKET: attachments

04S3 client settings

AWS SDK v3 (JavaScript) and boto3 1.28 or later read AWS_ENDPOINT_URL_S3 themselves. Your app must use path-style addressing (http://host/bucket/key, not http://bucket.host/key):

js
// AWS SDK v3
new S3Client({ forcePathStyle: true })
python
# boto3
boto3.client("s3", config=Config(s3={"addressing_style": "path"}))

An older SDK that doesn't read AWS_ENDPOINT_URL_S3: pass it as the client's endpoint yourself.

05GCS client settings

Point your client at STORAGE_EMULATOR_HOST. Google's Node (@google-cloud/storage) and Python (google-cloud-storage) clients read it themselves and skip authentication when it is set, so new Storage() and storage.Client() work with no credentials. On an older Python client that doesn't, pass both yourself:

python
storage.Client(
    credentials=AnonymousCredentials(),
    client_options={"api_endpoint": os.environ["STORAGE_EMULATOR_HOST"]},
)

For another language, set your client's endpoint to STORAGE_EMULATOR_HOST and give it anonymous credentials.

AWS_ENDPOINT_URL_S3 and STORAGE_EMULATOR_HOST are reachable only inside the sandbox. A presigned (S3) or signed (GCS) URL your app hands to a browser must use the object-storage port's public URL instead. Its origin is a { port } param, which you pass to your app:

yaml
params:
  storage_url: { port: object-storage }
env:
  STORAGE_PUBLIC_URL: ${params.storage_url}

The service that reads it needs reload_on_prepare: true, like any service that reads a param.

Anyone with that URL reaches the storage: S3 with the fixed credentials above, GCS with no authentication at all. It holds the sandbox's test data, like every other port of the sandbox. Never put real data in it.

S3 presigned URLs

Sign the URL against the public origin with a second client used only for presigning (signing makes no network call), and keep every real read and write on the default one:

js
const presigner = new S3Client({
  endpoint: process.env.STORAGE_PUBLIC_URL,
  forcePathStyle: true,
});
const url = await getSignedUrl(presigner,
  new GetObjectCommand({ Bucket: 'attachments', Key: key }));

The variable's name is yours: an app that already has such a setting (for example ATTACHMENTS_PUBLIC_ENDPOINT: ${params.storage_url}) just sets it. The public URL preserves the request's Host, so the signature holds, and browser uploads through a presigned PUT are allowed from any origin.

GCS signed URLs

Generate signed URLs with the public origin as their host: in Node, the host option of file.getSignedUrl; in Python, the api_access_endpoint argument of blob.generate_signed_url.

  • Signing still needs a credential that can sign, such as a service account key file. The emulator doesn't check signatures, so give the sandbox a throwaway key, never a real one.
  • Signed downloads and signed uploads work.
  • A browser must upload in one signed PUT. A resumable upload started from a browser is answered with an address inside the sandbox that the browser can't reach, so resumable uploads work only from inside the sandbox (your app's server, an action).

07Seeding files with an action

An action can put files in a bucket, so a seed can start every claim with them.

  • If your app records each file in its database (an attachment row pointing at the object), go through the app's own upload API instead, so the row exists too.
  • For a file the app only reads (a logo, an import template), write it straight to the bucket.

These examples need only curl (7.75 or later, for --aws-sigv4):

bash
#!/usr/bin/env bash
# sandbox/actions/add-template.sh  (S3)
set -euo pipefail
curl -fsS --aws-sigv4 "aws:amz:$AWS_REGION:s3" \
  --user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
  -H 'content-type: text/csv' -T sandbox/files/import-template.csv \
  "$AWS_ENDPOINT_URL_S3/exports/templates/import-template.csv" >&2
echo '{"key": "templates/import-template.csv"}'
bash
#!/usr/bin/env bash
# sandbox/actions/add-template.sh  (GCS)
set -euo pipefail
query='uploadType=media&name=templates/import-template.csv'
curl -fsS -X POST -H 'content-type: text/csv' \
  --data-binary @sandbox/files/import-template.csv \
  "$STORAGE_EMULATOR_HOST/upload/storage/v1/b/exports/o?$query" >&2
echo '{"key": "templates/import-template.csv"}'

Declare its output (key: { type: string }) and call it from a seed. See Seeds and actions.

08Limits

  • One provider per application.
  • GCS: one upload request carries at most 128 MB, a signed PUT from a browser included.
  • GCS: a browser uploads only through a single signed PUT.
  • The data lives only in the sandbox: nothing is copied from or to a real bucket, and it goes when the sandbox is destroyed.
  • It is reachable on the public object-storage URL: test data only.