Skip to content

fix(storage): make s3 disks work with S3-compatible endpoints (RustFS, MinIO) and AWS_* env - #2

Merged
vheins merged 5 commits into
rustasea:masterfrom
kangmaup:feat/storage-s3-compatible
Sep 24, 2026
Merged

vheins merged 5 commits into
rustasea:masterfrom
kangmaup:feat/storage-s3-compatible

Conversation

@kangmaup

Copy link
Copy Markdown
Contributor

Summary

s3 disks could not be used against an S3-compatible service in local development (RustFS, MinIO), and the AWS_* variables that .env.example documents were never read. This PR fixes both. It also redacts the S3 secret from Debug output, validates the bucket, adds a Docker-backed RustFS test suite and a storage-s3 umbrella feature, and makes CI compile the opt-in storage drivers.

Problems

  1. http:// endpoints always failed. object_store enforces HTTPS unless allow_http is set (ClientOptions builds its client with https_only(!allow_http)), and build_s3 never set it. The documented example in config/storage.toml (endpoint = "http://localhost:9000") failed every request:
    Error performing PUT http://127.0.0.1:32774/rustasea-test/reports/2026/hello.txt in 46.854µs - HTTP error: builder error
    
  2. AWS_* variables were ignored. .env.example says "The app reads the S3 credentials from the AWS_* variables above when the s3 disk is used", and the scaffolded docker-compose.yml passes AWS_ENDPOINT to the app. But StorageManager::from_toml_file parses the TOML as-is and the builder used AmazonS3Builder::new() (not from_env()), so nothing was read. A disk without inline credentials fell back to EC2 instance metadata:
    Error performing PUT http://169.254.169.254/latest/api/token in 12.781797967s, after 10 retries
    
  3. The secret leaked through Debug. S3DiskConfig derived Debug, so any debug dump of the storage config printed secret_access_key. SftpDiskConfig already redacts its password.
  4. CI never compiled the aws (or sftp) feature, so none of this was caught.

Changes

Five commits:

  1. refactor(storage), no behaviour change. S3DiskConfig moves verbatim to a new rustasea_storage::s3 module, mirroring sftp/config.rs, and stays re-exported from facade and the crate root. env_non_empty and the with_env test helper become shared.
  2. fix(storage):
    • Env overlay. apply_env() overlays AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, AWS_BUCKET, and AWS_ENDPOINT onto every s3 disk. Blank values are ignored and env wins over the file, the same semantics as the SFTP_* overlay.
    • Plain HTTP. An explicit http:// endpoint (surrounding whitespace ignored) enables allow_http. Any other endpoint, and AWS itself, stays HTTPS-only. No new config key.
    • Debug redaction. A manual Debug impl redacts secret_access_key.
    • Validation. validate() runs after the overlay, as for SFTP. bucket may be omitted when AWS_BUCKET supplies it, and a blank bucket is a StorageError::Config instead of a store with a malformed URL.
  3. feat(rustasea): new umbrella feature storage-s3 = ["rustasea-storage/aws"], next to storage-sftp, plus docs.
  4. ci: cargo xtask ci lints rustasea-storage --features aws,sftp, and the test job runs cargo test -p rustasea-storage --features aws,sftp. CONTRIBUTING and the README describe the new steps and the storage suites.
  5. docs: CHANGELOG.

Testing

  • Unit tests (no network):

    • env overlay mapping, and blank values ignored
    • allow_http per endpoint (http://, HTTP://, padded http://, https://, none)
    • Debug redaction
    • build_s3 applying the overlay (checked through the built disk's label)
    • a bucket supplied only by AWS_BUCKET
    • a blank bucket rejected

    Mutation-checked: skipping apply_env in build_s3, never enabling allow_http, and always enabling it each fail a test.

  • tests/rustfs_container.rs against rustfs/rustfs:1.0.0, #[ignore]d like the SFTP suite. It round-trips put/get/exists/delete over a plain-HTTP endpoint twice: once with inline credentials, once with a disk that names only its driver and gets everything else from AWS_*. Both tests fail on master with the errors above and pass with this PR.

    cargo test -p rustasea-storage --features aws --test rustfs_container -- --ignored
    
  • Other checks: cargo xtask ci, cargo test -p rustasea-storage --features aws,sftp, cargo +1.88.0 check --workspace, cargo deny check, and cargo audit pass locally.

  • cargo test --workspace --no-fail-fast: 3 failures, and all three also fail on master (c501fdd) without this PR. They are why the Test (workspace) CI job is currently red there:

    • artisan_alias_generates_livewire_app: crates/rustasea/tests/cli.rs:146 still expects features = ["view", "broadcast"]. The scaffold has emitted ["view", "action"] since ab02ac6, and the umbrella has no broadcast feature.
    • routes::tests::two_factor::enable_confirm_challenge_and_recovery_lifecycle: login hits 429, because the login rate limiter is a process-wide static shared across tests.
    • tests::workers_renders_heartbeats in rustasea-queue-dashboard.

    I'll send fixes for those in a separate PR.

Notes

  • Behaviour change. AWS_* values now override the matching keys in config/storage.toml. The stock .env.example sets AWS_DEFAULT_REGION=us-east-1, so the region is changed there. With more than one s3 disk, AWS_BUCKET/AWS_ENDPOINT would override every disk; the docs say to leave them blank in that case. Happy to scope the overlay to a single disk if you prefer.
  • MSRV. The aws feature needs rustc 1.89. object_store's AWS backend depends on crc-fast, and the lockfile has 1.10.0, which declares rust-version = 1.89. This predates this PR, which only documents it.
    • Pinning crc-fast to 1.9.0 (MSRV 1.81) is possible but also forces crc down to 3.3.0 under sqlx-core.
    • A lockfile pin does not help downstream apps, which resolve their own lockfile.
    • So I left the lockfile alone. Tell me if you would rather pin.
  • Cargo.lock is unchanged. The RustFS suite sends its signed CreateBucket request through object_store's own HTTP client.
  • Possible follow-ups, out of scope here: temporary_url (presigned URLs), AWS_SESSION_TOKEN and IRSA/ECS credential chains, and swapping the docker-compose minio service for RustFS.

…v helpers

Moves `S3DiskConfig` verbatim from `facade.rs` into a new
`rustasea_storage::s3` module, mirroring `sftp/config.rs`, so the S3 fix
that follows has a home of its own. It stays re-exported from `facade` and
the crate root, so existing paths keep working.

`env_non_empty` moves from `sftp/config.rs` to `facade.rs`, and the
`with_env` test helper (with its env lock) moves from `sftp/tests.rs` to a
shared `test_support` module, ready for the S3 overlay. No behaviour change.
…s3 disks

`s3` disks could not talk to an S3-compatible service in local development:

- `object_store` only allows HTTPS unless `allow_http` is set, and
  `build_s3` never set it, so the documented
  `endpoint = "http://localhost:9000"` example failed every request with a
  reqwest builder error. An explicit `http://` endpoint (surrounding
  whitespace ignored) now enables `allow_http`; every other endpoint, and
  AWS itself, stays HTTPS-only.
- `.env.example` documents that the `s3` disk reads `AWS_ACCESS_KEY_ID`,
  `AWS_SECRET_ACCESS_KEY`, `AWS_DEFAULT_REGION`, `AWS_BUCKET`, and
  `AWS_ENDPOINT` (the scaffolded docker-compose passes `AWS_ENDPOINT` to
  the app), but `StorageManager::from_toml_file` parses the file as-is and
  the builder used `AmazonS3Builder::new()`, so none were read and a disk
  without inline credentials fell back to EC2 instance metadata.
  `S3DiskConfig::apply_env` now overlays them onto every `s3` disk,
  mirroring the `SFTP_*` overlay.

Like `SftpDiskConfig`, the config now redacts `secret_access_key` from
`Debug` output and is validated after the overlay: `bucket` may be omitted
when `AWS_BUCKET` supplies it, and a blank bucket is a `Config` error
instead of a store with a malformed URL.

A Docker-backed RustFS suite (`tests/rustfs_container.rs`, `#[ignore]`d
like the SFTP one) reproduces both failures on the unfixed code and passes
with the fix.
Forwards to `rustasea-storage/aws`, next to `storage-sftp`, so apps built
on the facade crate can enable `s3` disks without depending on
`rustasea-storage` directly. Documents the feature in the README feature
table, the storage crate README, and `config/storage.toml`, including that
the `aws` feature needs rustc 1.89+: `object_store`'s AWS backend pulls in
`crc-fast` 1.10, whose MSRV is above the workspace's 1.88.
The `s3` and `sftp` disks sit behind `rustasea-storage`'s opt-in `aws` and
`sftp` features, which the default workspace build never compiles, so a
broken driver could not fail CI. `cargo xtask ci` now runs clippy on
`rustasea-storage` with `--features aws,sftp`, and the test job runs its
tests with both features. The Docker-backed RustFS and SFTP suites stay
`#[ignore]`d. CONTRIBUTING and the README describe the new steps and the
storage suites.
@kangmaup
kangmaup marked this pull request as draft September 24, 2026 06:36
@kangmaup
kangmaup marked this pull request as ready for review September 24, 2026 06:37
@kangmaup

Copy link
Copy Markdown
Contributor Author

Heads-up on the red checks: none of them come from this PR, and all of them also fail on master:

  • Test (workspace): three tests are already broken on master (see "Testing" above). This PR's new step, cargo test -p rustasea-storage --features aws,sftp, was skipped because the earlier step failed.
  • cargo-deny: since TASK-115 the workflow sets RUSTC_WRAPPER: sccache for every job, but the deny job doesn't install sccache. cargo deny runs cargo metadata, which runs sccache rustc -vV and fails. cargo-audit passes because it never calls cargo metadata. Reproduced locally with RUSTC_WRAPPER=sccache cargo deny check.
  • Assign vheins as reviewer: pull_request runs from forks only get a read-only token, so requestReviewers fails with "Resource not accessible by integration". pull_request_target would fix it, since the job never checks out PR code.

I'll open a separate PR that fixes all three so master is green again.

@vheins
vheins self-requested a review September 24, 2026 08:26
@vheins
vheins merged commit 393c3e4 into rustasea:master Sep 24, 2026
3 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants