diff --git a/rustfs/src/storage/ecfs.rs b/rustfs/src/storage/ecfs.rs index f7e4d6944..cf0a27c7e 100644 --- a/rustfs/src/storage/ecfs.rs +++ b/rustfs/src/storage/ecfs.rs @@ -116,7 +116,7 @@ use rustfs_utils::{ http::{ AMZ_BUCKET_REPLICATION_STATUS, AMZ_CHECKSUM_MODE, AMZ_CHECKSUM_TYPE, headers::{ - AMZ_DECODED_CONTENT_LENGTH, AMZ_OBJECT_TAGGING, AMZ_RESTORE_EXPIRY_DAYS, AMZ_RESTORE_REQUEST_DATE, + AMZ_DECODED_CONTENT_LENGTH, AMZ_OBJECT_TAGGING, AMZ_RESTORE_EXPIRY_DAYS, AMZ_RESTORE_REQUEST_DATE, AMZ_TAG_COUNT, RESERVED_METADATA_PREFIX, RESERVED_METADATA_PREFIX_LOWER, }, }, @@ -3040,6 +3040,15 @@ impl S3 for FS { let content_language = metadata_map.get("content-language").cloned(); let expires = info.expires.map(Timestamp::from); + // Calculate tag count from user_tags already in ObjectInfo + // This avoids an additional API call since user_tags is already populated by get_object_info + let tag_count = if !info.user_tags.is_empty() { + let tag_set = decode_tags(&info.user_tags); + tag_set.len() + } else { + 0 + }; + let output = HeadObjectOutput { content_length: Some(content_length), content_type, @@ -3076,7 +3085,19 @@ impl S3 for FS { // interact with objects (PUT/POST/DELETE/LIST, etc.) rely on the system-level // CORS layer instead. In case both are applicable, this bucket-level CORS logic // takes precedence for these read operations. - let response = wrap_response_with_cors(&bucket, &req.method, &req.headers, output).await; + let mut response = wrap_response_with_cors(&bucket, &req.method, &req.headers, output).await; + + // Add x-amz-tagging-count header if object has tags + // Per S3 API spec, this header should be present in HEAD object response when tags exist + if tag_count > 0 { + let header_name = http::HeaderName::from_static(AMZ_TAG_COUNT); + if let Ok(header_value) = tag_count.to_string().parse::() { + response.headers.insert(header_name, header_value); + } else { + warn!("Failed to parse x-amz-tagging-count header value, skipping"); + } + } + let result = Ok(response); let _ = helper.complete(&result); diff --git a/scripts/s3-tests/README.md b/scripts/s3-tests/README.md index f3c0ab579..b4f4c496e 100644 --- a/scripts/s3-tests/README.md +++ b/scripts/s3-tests/README.md @@ -12,11 +12,20 @@ Run the local S3 compatibility test script: The script will automatically: -1. Build RustFS (if needed) -2. Start the service -3. Wait for it to be ready -4. Run S3 compatibility tests -5. Collect results in `artifacts/s3tests-single/` +1. **Check prerequisites**: Verify port availability (unless in existing mode) +2. **Build/Start RustFS**: + - Build mode (default): Compile with `cargo build --release` (skips if binary is recent < 30 min) + - Binary mode: Use specified or default binary path + - Docker mode: Build Docker image and start container + - Existing mode: Skip startup, connect to running service +3. **Wait for readiness**: Multi-step health check (process/container → port → log → S3 API) +4. **Prepare environment**: + - Generate s3tests configuration from template + - Provision alt user via admin API + - Clone s3-tests repository if missing + - Install missing dependencies (awscurl, tox, gettext) +5. **Run tests**: Execute ceph s3-tests via tox with configured filters +6. **Collect results**: Save logs and test results in `artifacts/s3tests-${TEST_MODE}/` ## Deployment Modes @@ -35,7 +44,13 @@ DEPLOY_MODE=build ./scripts/s3-tests/run.sh ./scripts/s3-tests/run.sh --no-cache ``` -**Note**: In build mode, if the binary exists and was compiled less than 30 minutes ago, compilation will be skipped unless `--no-cache` is specified. +**Behavior**: +- Automatically compiles RustFS binary if it doesn't exist +- If binary exists and was compiled less than **30 minutes** ago, compilation is skipped (unless `--no-cache` is specified) +- Automatically starts the service after compilation +- Automatically clones s3-tests repository if missing +- Automatically installs missing dependencies (awscurl, tox, gettext) +- **Automatic Cleanup**: Process is automatically stopped when script exits ### 2. Binary File Mode @@ -49,6 +64,13 @@ DEPLOY_MODE=binary ./scripts/s3-tests/run.sh DEPLOY_MODE=binary RUSTFS_BINARY=./target/release/rustfs ./scripts/s3-tests/run.sh ``` +**Behavior**: +- Uses existing binary file (must exist, script will not compile) +- Automatically starts the service using the specified binary +- Automatically clones s3-tests repository if missing +- Automatically installs missing dependencies (awscurl, tox, gettext) +- **Automatic Cleanup**: Process is automatically stopped when script exits + ### 3. Docker Mode Build Docker image and run in container: @@ -57,6 +79,13 @@ Build Docker image and run in container: DEPLOY_MODE=docker ./scripts/s3-tests/run.sh ``` +**Behavior**: +- Automatically builds Docker image using `Dockerfile.source` +- Creates Docker network (`rustfs-net`) if it doesn't exist +- Automatically clones s3-tests repository if missing +- Automatically installs missing dependencies (awscurl, tox, gettext) +- **Automatic Cleanup**: Container and network are automatically removed when script exits + ### 4. Existing Service Mode Connect to an already running RustFS service: @@ -68,6 +97,18 @@ DEPLOY_MODE=existing S3_HOST=127.0.0.1 S3_PORT=9000 ./scripts/s3-tests/run.sh DEPLOY_MODE=existing S3_HOST=192.168.1.100 S3_PORT=9000 ./scripts/s3-tests/run.sh ``` +**Behavior**: +- Skips service startup and port availability checks +- Connects directly to the specified service endpoint +- Automatically clones s3-tests repository if missing +- Automatically installs missing dependencies (awscurl, tox, gettext) +- **Note**: The service must already have the alt user (`rustfsalt`) provisioned, or the script will provision it automatically + +**Automatic Cleanup**: The script uses trap handlers to automatically clean up resources when it exits (success or failure): +- Stops RustFS process (build/binary mode) +- Stops and removes Docker container (docker mode) +- Removes Docker network (docker mode) + ## Configuration Options ### Command Line Options @@ -102,7 +143,10 @@ DEPLOY_MODE=existing S3_HOST=192.168.1.100 S3_PORT=9000 ./scripts/s3-tests/run.s - `TEST_MODE`: Test mode (default: `single`) - `MAXFAIL`: Stop after N failures (default: `1`) - `XDIST`: Enable parallel execution with N workers (default: `0`, disabled) -- `MARKEXPR`: pytest marker expression for filtering tests (default: exclude unsupported features) +- `MARKEXPR`: pytest marker expression for filtering tests + - Default: `not lifecycle and not versioning and not s3website and not bucket_logging and not encryption` + - Excludes features not yet supported by RustFS to reduce test execution time + - Can be customized to test specific features or remove exclusions ### Configuration Files @@ -119,19 +163,28 @@ DEPLOY_MODE=existing S3_HOST=192.168.1.100 S3_PORT=9000 ./scripts/s3-tests/run.s ```bash # Basic usage - compiles and runs automatically +# Skips compilation if binary exists and is less than 30 minutes old ./scripts/s3-tests/run.sh -# Force rebuild (skip cache check) +# Force rebuild (skip cache check, always compile) ./scripts/s3-tests/run.sh --no-cache # Run all tests, stop after 50 failures MAXFAIL=50 ./scripts/s3-tests/run.sh # Enable parallel execution (4 worker processes) +# Automatically installs pytest-xdist if needed XDIST=4 ./scripts/s3-tests/run.sh # Use custom data storage location +# Data will be stored in /tmp/test-data/rustfs-single/ DATA_ROOT=/tmp ./scripts/s3-tests/run.sh + +# Run specific test markers (e.g., test multipart uploads only) +MARKEXPR="multipart" ./scripts/s3-tests/run.sh + +# Remove feature exclusions (test all features, including unsupported ones) +MARKEXPR="" ./scripts/s3-tests/run.sh ``` ### Binary File Mode @@ -163,14 +216,20 @@ DEPLOY_MODE=docker XDIST=4 ./scripts/s3-tests/run.sh ### Existing Service Mode ```bash -# Connect to locally running service +# Connect to locally running service (default: 127.0.0.1:9000) DEPLOY_MODE=existing ./scripts/s3-tests/run.sh # Connect to remote service DEPLOY_MODE=existing S3_HOST=192.168.1.100 S3_PORT=9000 ./scripts/s3-tests/run.sh -# Test specific features +# Test specific features (custom marker expression) DEPLOY_MODE=existing MARKEXPR="not lifecycle and not versioning" ./scripts/s3-tests/run.sh + +# Use custom credentials +DEPLOY_MODE=existing \ + S3_ACCESS_KEY=myaccesskey \ + S3_SECRET_KEY=mysecretkey \ + ./scripts/s3-tests/run.sh ``` ### Custom Configuration Files @@ -206,25 +265,109 @@ less artifacts/s3tests-single/rustfs-single/rustfs.log ## Prerequisites -### Required +### Required System Dependencies -- Docker (for docker mode only) -- Python 3 (for running s3-tests) -- `nc` (netcat) or `timeout` command (for port checking) +The following dependencies must be installed manually on your system: -### Auto-installed +#### All Deployment Modes -The script will automatically install the following dependencies if missing: +- **Python 3**: Required for running s3-tests + - Check: `python3 --version` + - Install: + - macOS: Usually pre-installed, or `brew install python3` + - Linux: `apt-get install python3` or `yum install python3` -- `awscurl` (for S3 API calls and user provisioning) -- `tox` (for running s3-tests in isolated environment) -- `gettext-base` (for `envsubst` - config file generation) - - On macOS: `brew install gettext` - - On Linux: `apt-get install gettext-base` +- **Git**: Required for cloning s3-tests repository + - Check: `git --version` + - Install: + - macOS: Usually pre-installed, or `brew install git` + - Linux: `apt-get install git` or `yum install git` + +- **Port checking tools**: One of the following for port availability checks + - `nc` (netcat): `apt-get install netcat` or `brew install netcat` + - OR `timeout` command: Usually pre-installed on Linux + - OR bash built-in TCP redirection support + +#### Docker Mode Only + +- **Docker**: Required only when using `DEPLOY_MODE=docker` + - Check: `docker --version` + - Install: [Docker Installation Guide](https://docs.docker.com/get-docker/) + +#### Build Mode Only + +- **Rust toolchain**: Required when using `DEPLOY_MODE=build` (default) + - Check: `rustc --version` and `cargo --version` + - Install: [Rust Installation Guide](https://www.rust-lang.org/tools/install) + +### Auto-installed Dependencies + +The script will automatically install the following dependencies if missing (no manual action required): + +- **awscurl**: For S3 API calls and user provisioning + - Installed via: `python3 -m pip install --user --upgrade pip awscurl` + - Location: `$HOME/.local/bin/awscurl` + +- **tox**: For running s3-tests in isolated Python environment + - Installed via: `python3 -m pip install --user --upgrade pip tox` + - Location: `$HOME/.local/bin/tox` + +- **gettext-base**: For `envsubst` command (config file generation) + - macOS: Automatically installs via `brew install gettext` + - Linux: Automatically installs via `sudo apt-get install gettext-base` + - **Note**: macOS installation may require manual intervention if brew fails + +- **s3-tests repository**: Automatically cloned if not present + - Source: `https://github.com/ceph/s3-tests.git` + - Location: `${PROJECT_ROOT}/s3-tests` + +**Note**: The script adds `$HOME/.local/bin` to `PATH` automatically, so auto-installed Python tools are accessible. ### Proxy Configuration -The script automatically disables proxy for localhost requests to avoid interference. All proxy environment variables (`http_proxy`, `https_proxy`, etc.) are unset at script startup. +The script automatically disables proxy for localhost requests to avoid interference. All proxy environment variables (`http_proxy`, `https_proxy`, `HTTP_PROXY`, `HTTPS_PROXY`) are unset at script startup. The `NO_PROXY` variable is set to `127.0.0.1,localhost,::1`. + +## Cleanup + +The test script automatically cleans up processes and containers when it exits. However, if you need to manually clean up: + +### Using the Cleanup Script + +A dedicated cleanup script is available to clean up test resources: + +```bash +# Clean up port 9000 and test data directory +./scripts/s3-tests/cleanup.sh + +# Use custom port and host +S3_PORT=9001 S3_HOST=127.0.0.1 ./scripts/s3-tests/cleanup.sh +``` + +The cleanup script will: +- Kill any process using the specified port (default: 9000) +- Clean test data directory at `target/test-data/rustfs-single/` +- Verify port is released + +### Manual Cleanup + +If the cleanup script doesn't work or you need more control: + +```bash +# Kill process on port 9000 +lsof -ti:9000 | xargs kill -9 + +# Or use netstat/ss +kill -9 $(netstat -tuln | grep :9000 | awk '{print $7}' | cut -d'/' -f1) + +# Remove test data +rm -rf target/test-data/rustfs-single/ + +# Stop Docker container (if using docker mode) +docker rm -f rustfs-single + +# Remove Docker network (if using docker mode) +docker network rm rustfs-net +``` ## Troubleshooting diff --git a/scripts/s3-tests/implemented_tests.txt b/scripts/s3-tests/implemented_tests.txt index 8034aec0d..a5122b4dc 100644 --- a/scripts/s3-tests/implemented_tests.txt +++ b/scripts/s3-tests/implemented_tests.txt @@ -120,6 +120,7 @@ test_object_copy_bucket_not_found test_object_copy_key_not_found test_object_copy_not_owned_object_bucket test_object_head_zero_bytes +test_get_obj_head_tagging test_object_metadata_replaced_on_put test_object_put_authenticated test_object_read_not_exist diff --git a/scripts/s3-tests/unimplemented_tests.txt b/scripts/s3-tests/unimplemented_tests.txt index 27a35ee1c..66a210755 100644 --- a/scripts/s3-tests/unimplemented_tests.txt +++ b/scripts/s3-tests/unimplemented_tests.txt @@ -167,7 +167,6 @@ test_ignore_public_acls # PublicAccessBlock and tag validation tests test_block_public_policy test_block_public_policy_with_principal -test_get_obj_head_tagging test_get_public_block_deny_bucket_policy test_get_undefined_public_block test_put_excess_key_tags