mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-29 17:48:58 +00:00
Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f07029373 | |||
| 4b36667ba1 | |||
| 41ba34a145 | |||
| 3898d524fe | |||
| 718bec7722 | |||
| 50ddec3ffc | |||
| 7692c0c3bd | |||
| 3dd0692917 | |||
| 090d60e00a | |||
| c228fabfdb | |||
| bde569d055 | |||
| e19b0dd997 | |||
| 26b3aad069 | |||
| bf893bcb55 | |||
| 60d4598562 | |||
| 565cbdffed | |||
| 49b2782d51 | |||
| 743b87014b | |||
| 3208f930c5 | |||
| 36b3d21c44 | |||
| 935bb8a8e2 | |||
| 9364ecba67 | |||
| 0153710791 | |||
| 995e26f5ee | |||
| e6fdcd1ad6 | |||
| f016673416 | |||
| 42ff6b4d80 | |||
| 3ced40f221 | |||
| a830ab2a5d | |||
| 52f121ab2a | |||
| eb8868397e | |||
| 66c38b629d | |||
| eb23710d2e | |||
| 4b66155f26 | |||
| 4978f60254 | |||
| fe59eb4952 | |||
| a455b4377c | |||
| 08fab34804 | |||
| f07fed0c49 | |||
| dcfbc2612a | |||
| 87e1c7aeb6 | |||
| 0d7e0a814f | |||
| ef5ccc232a | |||
| c29c8a5a1e | |||
| 403997f2f8 | |||
| e34d75dfdf | |||
| 132e72ef39 | |||
| 4f4c759b67 | |||
| e331a26262 | |||
| 61d2e9fbc3 | |||
| 946755aa89 | |||
| c9622611ed | |||
| 7041e628b7 | |||
| 8e1bd560d8 | |||
| d447da75c1 | |||
| 2c9524e2c9 | |||
| e16f1ae639 | |||
| d79720da1d | |||
| b747e9817e | |||
| 90ce72122b | |||
| c4d5c5c5ec | |||
| c698a0f2b6 | |||
| 2953558f41 | |||
| e0b8c4fd42 | |||
| a995ec0315 | |||
| 946b502527 | |||
| 09be06a4d2 | |||
| 159ddd5bac | |||
| a68fe1601f | |||
| 334184b005 | |||
| cfbd094bc4 | |||
| 468dc3aebd | |||
| fd258e877c | |||
| 4dafb64d58 | |||
| 50d03ef021 | |||
| 438913b84a | |||
| 37a3cbc497 | |||
| a05687b900 | |||
| 561d695237 | |||
| 59f41eb86a | |||
| 81854762d4 | |||
| 1e9c75a201 | |||
| 80413e0f8e | |||
| b96ccfd110 | |||
| c717195de2 | |||
| 94f64acc87 | |||
| d949d4e794 | |||
| 7215761784 | |||
| f833cd9cbe | |||
| 13b4500212 | |||
| 2705e3f53b | |||
| 92f812fc80 | |||
| fefb308b35 | |||
| 8d4caeacad | |||
| 2860c82e3c | |||
| 572dd1264e | |||
| 47247789ad | |||
| 39f7de4450 | |||
| de6fe816c2 | |||
| 368ef0f16c | |||
| bc37cc4001 | |||
| ecf0db9bb7 | |||
| fa1554be7f | |||
| f08b592c6f | |||
| 8add0126f5 | |||
| 09a83a8f56 | |||
| a0f1bb4ff0 | |||
| 3ac1d2ab0b |
@@ -0,0 +1,113 @@
|
|||||||
|
---
|
||||||
|
name: security-advisory-lessons
|
||||||
|
description: Apply RustFS security lessons distilled from repository GitHub Security Advisories. Use when making or reviewing RustFS code changes, doing security checks, handling PR review for auth/authz, IAM, storage, RPC, logging, CORS, console/browser, encryption, policy, or endpoint changes, and when deciding which security regression tests are required.
|
||||||
|
---
|
||||||
|
|
||||||
|
# RustFS Security Advisory Lessons
|
||||||
|
|
||||||
|
Use this skill as a RustFS-specific security lens before changing or approving code. For the current advisory snapshot and full pattern map, read [advisory-patterns.md](references/advisory-patterns.md).
|
||||||
|
|
||||||
|
When currentness matters, refresh the advisory inventory first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh api repos/rustfs/rustfs/security-advisories --paginate \
|
||||||
|
--jq '.[] | {ghsa_id,state,severity,summary,updated_at,html_url}'
|
||||||
|
```
|
||||||
|
|
||||||
|
For the full pattern map, read [advisory-patterns.md](references/advisory-patterns.md).
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### 1. Scope the change
|
||||||
|
- Identify touched routes, handlers, storage paths, credentials, logs, browser surfaces, CI/release code, and policy checks.
|
||||||
|
- Treat these paths as security-sensitive by default: `rustfs/src/admin/`, `rustfs/src/storage/`, `rustfs/src/auth.rs`, `rustfs/src/server/layer.rs`, `crates/iam/`, `crates/policy/`, `crates/credentials/`, `crates/ecstore/src/rpc/`, `crates/rio/`, and console preview/auth code.
|
||||||
|
|
||||||
|
### 2. Map to advisory classes
|
||||||
|
- Read [advisory-patterns.md](references/advisory-patterns.md) for matching GHSA lessons.
|
||||||
|
- Do not rely on advisory titles alone. Confirm whether the issue is authentication, authorization, input validation, storage invariant, browser isolation, logging, or operational hardening.
|
||||||
|
|
||||||
|
### 3. Verify fail-closed behavior
|
||||||
|
- Check that unauthenticated, wrong-permission, cross-user, cross-bucket, malformed-input, and default-config cases fail explicitly.
|
||||||
|
- Prefer exact action/permission checks over broad helper calls or inferred ownership.
|
||||||
|
- Confirm lower storage/RPC layers do not bypass checks done in upper layers.
|
||||||
|
|
||||||
|
### 4. Require regression evidence
|
||||||
|
- For behavior changes, add focused negative tests that reproduce the advisory class.
|
||||||
|
- For sensitive fixes, include tests for the bypass form, not only the happy path.
|
||||||
|
- If a test is impractical, explain the residual risk and provide a manual verification command.
|
||||||
|
|
||||||
|
### 5. Report clearly
|
||||||
|
- Lead with concrete findings and file/line evidence.
|
||||||
|
- Separate proven vulnerabilities from hardening risks.
|
||||||
|
- Avoid exaggerating unauthenticated impact when the code actually rejects unauthenticated requests but allows a low-privileged authenticated bypass.
|
||||||
|
|
||||||
|
## Advisory-Derived Guardrails
|
||||||
|
|
||||||
|
### Auth and admin authorization
|
||||||
|
- Every admin or diagnostic route needs an explicit authn and authz story. Route registration, router whitelist, and handler-level authorization must agree.
|
||||||
|
- Match the admin action to the operation exactly. Copy-paste action constants are a known RustFS vulnerability class.
|
||||||
|
- Avoid authentication-only helpers for state-changing admin APIs; use `validate_admin_request` or the established equivalent with the right `AdminAction`.
|
||||||
|
- Do not assume admin-action `Resource` scoping constrains blast radius unless the policy engine actually enforces resources for that action.
|
||||||
|
|
||||||
|
### IAM and service accounts
|
||||||
|
- Treat imported IAM payload fields as attacker-controlled: `parent`, `claims`, `accessKey`, `secretKey`, status, policy names, and groups.
|
||||||
|
- For service account create/update/import, prove parent ownership or root/admin authority before writing credentials or claims.
|
||||||
|
- Do not let `deny_only` or "no explicit deny" become an allow decision that skips required allow checks.
|
||||||
|
- Test cross-user list/update/import flows with wrong, correct, self, parent, and root identities.
|
||||||
|
|
||||||
|
### S3 copy, multipart, and presigned POST
|
||||||
|
- Multipart copy must enforce source `GetObject` and destination `PutObject` semantics equivalent to `CopyObject`, including copy-source and policy conditions.
|
||||||
|
- Do not let `CreateMultipartUpload`, `UploadPartCopy`, `CompleteMultipartUpload`, or `AbortMultipartUpload` return success without authorization.
|
||||||
|
- Presigned POST policies are server-side contracts. Enforce `content-length-range`, key prefix, exact metadata/content-type, and all signed policy conditions.
|
||||||
|
|
||||||
|
### Paths, object keys, and filesystem access
|
||||||
|
- Never join untrusted bucket/object/RPC path strings onto filesystem roots without normalization and boundary checks.
|
||||||
|
- Reject or safely handle `..`, absolute paths, URL-encoded traversal, platform separators, empty components, and paths that canonicalize outside the intended root.
|
||||||
|
- Validate both S3 object-key paths and internode/RPC disk paths; storage helpers can bypass S3 authorization if they trust already-parsed paths.
|
||||||
|
|
||||||
|
### Secrets, default credentials, and crypto
|
||||||
|
- Do not ship hard-coded shared tokens, HMAC secrets, private keys, or production test keys.
|
||||||
|
- Defaults for internode/RPC auth must fail closed for network-reachable deployments or require explicit opt-in with loud warnings.
|
||||||
|
- License or token validation must use signatures with embedded public/verifying keys only; do not use private-key decryption as authenticity.
|
||||||
|
- Plan key rotation and key IDs when removing exposed keys.
|
||||||
|
|
||||||
|
### Logging and debug output
|
||||||
|
- Logs must never include access keys beyond safe identifiers, secret keys, session tokens, JWT claims, HMAC secrets, expected signatures, license secrets, or raw response bodies containing credentials.
|
||||||
|
- Treat `Debug` implementations, `?value` tracing, merged config dumps, and dependency-level HTTP body logging as leak surfaces.
|
||||||
|
- Add log-capture tests or targeted unit tests for redaction wrappers when changing credential structs or response bodies.
|
||||||
|
|
||||||
|
### RPC, parsing, and panic safety
|
||||||
|
- Treat all RPC payload bytes as attacker-controlled. Replace `unwrap`, `expect`, and panic-prone deserialization with typed errors.
|
||||||
|
- Malformed request tests should cover empty bytes, truncated MessagePack/protobuf, invalid enum values, stale timestamps, and invalid signatures.
|
||||||
|
- RPC authentication must be independently strong; do not depend on S3 admin credentials unless the fallback is explicit and safe.
|
||||||
|
|
||||||
|
### Browser, CORS, and console surfaces
|
||||||
|
- Do not reflect arbitrary `Origin` while also allowing credentials. Default CORS should be no CORS unless explicitly configured.
|
||||||
|
- Do not render user-controlled object content in a same-origin iframe with console credentials available to JavaScript.
|
||||||
|
- Prefer origin separation for object preview/download, `nosniff`, CSP, strict content-type handling, and avoiding durable credentials in `localStorage`.
|
||||||
|
- License/version-like metadata endpoints should expose only coarse public data unless authenticated.
|
||||||
|
|
||||||
|
### Profiling, debug, and health endpoints
|
||||||
|
- Profiling and debug endpoints are not health checks. They require admin auth, opt-in enablement, rate limiting, and safe responses.
|
||||||
|
- Do not return absolute filesystem paths or other deployment layout in unauthenticated or low-privilege responses.
|
||||||
|
- Ensure health endpoint allowlists cannot accidentally include expensive diagnostics.
|
||||||
|
|
||||||
|
### Trusted proxy and network identity
|
||||||
|
- Only honor `X-Forwarded-For` or `X-Real-IP` when the request came from a configured trusted proxy.
|
||||||
|
- Direct clients must use the socket peer address for `aws:SourceIp` and policy condition evaluation.
|
||||||
|
- Add tests for direct spoofed headers and trusted-proxy headers.
|
||||||
|
|
||||||
|
### SSE and storage invariants
|
||||||
|
- Encryption metadata is not proof that bytes were encrypted on disk.
|
||||||
|
- When touching reader/writer wrappers such as hashing, encryption, compression, or warp readers, verify wrapper order and inspect stored bytes in regression tests.
|
||||||
|
- Avoid helper shortcuts that unwrap nested readers and accidentally bypass encryption or integrity layers.
|
||||||
|
|
||||||
|
## Review Prompts
|
||||||
|
|
||||||
|
Use these prompts while reviewing a diff:
|
||||||
|
|
||||||
|
- Could a low-privileged authenticated user reach this path with the wrong action, parent, bucket, or source object?
|
||||||
|
- Does a public/default/empty config change security behavior from fail-closed to fail-open?
|
||||||
|
- Is any attacker-controlled value later used as a path, policy condition, credential identity, log field, URL, Origin, or response body?
|
||||||
|
- Is the same operation implemented in multiple paths, such as `CopyObject` vs `UploadPartCopy`, and do all paths enforce the same security contract?
|
||||||
|
- Does the test prove the exploit form is denied, or only that the intended form still works?
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Security Advisory Lessons"
|
||||||
|
short_description: "Apply advisory lessons in reviews."
|
||||||
|
default_prompt: "Review code changes against past RustFS security advisory lessons and report concrete risks, missing tests, and recommended fixes."
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# RustFS Advisory Pattern Map
|
||||||
|
|
||||||
|
Snapshot source: `gh api repos/rustfs/rustfs/security-advisories --paginate` on 2026-05-05. It included 23 advisories: 8 triage, 13 published, and 2 closed.
|
||||||
|
|
||||||
|
Refresh this file when new advisories appear or when an advisory changes state materially.
|
||||||
|
|
||||||
|
## Pattern Index
|
||||||
|
|
||||||
|
### Admin authorization and route exposure
|
||||||
|
|
||||||
|
- `GHSA-pfcq-4gjr-6gjm` published: notification target endpoints accepted authenticated users but skipped admin authorization. Lesson: distinguish authn from authz; admin target CRUD must call the operation-specific admin authorization path.
|
||||||
|
- `GHSA-mm2q-qcmx-gw4w` published: `ListServiceAccount` used `UpdateServiceAccountAdminAction`, while update lacked target ownership checks. Lesson: exact action constants and ownership checks are both required; information disclosure can chain into secret rotation and takeover.
|
||||||
|
- `GHSA-vcwh-pff9-64cc` published: `ImportIam` checked `ExportIAMAction` for an import/write operation. Lesson: every admin handler must authorize the action it actually performs.
|
||||||
|
- `GHSA-jqmc-mg33-v45g` triage and `GHSA-8784-9m7f-c6p6` triage: `/profile/cpu` and `/profile/memory` were whitelisted from auth and allowed expensive diagnostics plus path disclosure. Lesson: profiling/debug endpoints need admin auth, opt-in, rate limits, and non-sensitive responses.
|
||||||
|
- `GHSA-x5xv-223c-8vm7` triage: console license metadata endpoint was public. Lesson: public metadata endpoints should be coarse or authenticated.
|
||||||
|
|
||||||
|
### IAM import, service accounts, and privilege boundaries
|
||||||
|
|
||||||
|
- `GHSA-566f-q62r-wcr8` triage: `ImportIam` accepted attacker-controlled service account `parent`, `claims`, `accessKey`, and `secretKey`, enabling persistent backdoor accounts under root. Lesson: imported IAM payloads are untrusted data and must be validated against privilege boundaries.
|
||||||
|
- `GHSA-xgr5-qc6w-vcg9` published: `deny_only=true` skipped allow checks and let restricted service accounts mint unrestricted children. Lesson: deny-only logic must never become implicit allow for privilege creation.
|
||||||
|
- `GHSA-mm2q-qcmx-gw4w` published: leaked service account access keys plus update-without-ownership formed an escalation chain. Lesson: service-account identifiers are security-sensitive because update APIs consume them.
|
||||||
|
|
||||||
|
### S3 copy, multipart, and upload policy validation
|
||||||
|
|
||||||
|
- `GHSA-mx42-j6wv-px98` published: `UploadPartCopy` missed source authorization and allowed cross-bucket object exfiltration. Lesson: multipart copy must enforce the same source and destination contract as `CopyObject`.
|
||||||
|
- `GHSA-wfxj-ph3v-7mjf` triage: `UploadPartCopy` checked source and destination independently but missed destination copy-source policy constraints. Lesson: source read and destination write checks are not sufficient when policy constrains allowed copy sources.
|
||||||
|
- `GHSA-w5fh-f8xh-5x3p` published: presigned POST accepted uploads without enforcing signed policy conditions. Lesson: parse and enforce all POST policy constraints server-side, including size, key prefix, and content type.
|
||||||
|
|
||||||
|
### Filesystem paths and object key traversal
|
||||||
|
|
||||||
|
- `GHSA-pq29-69jg-9mxc` published: RPC `read_file_stream` joined untrusted paths under a volume directory without canonical boundary checks. Lesson: `PathBuf::join` plus length checks are not path security.
|
||||||
|
- `GHSA-8r6f-hmq2-28rg` closed: object keys containing traversal sequences bypassed bucket/object authorization when mapped to filesystem paths. Lesson: reject traversal at object-key parsing and verify final storage paths remain under the expected bucket/key root.
|
||||||
|
|
||||||
|
### Secrets, defaults, and cryptographic misuse
|
||||||
|
|
||||||
|
- `GHSA-h956-rh7x-ppgj` published: gRPC used the hard-coded token `rustfs rpc` on both client and server. Lesson: source-visible shared tokens are authentication bypasses.
|
||||||
|
- `GHSA-r5qv-rc46-hv8q` triage: internode RPC HMAC secret fell back to the public default `rustfsadmin`. Lesson: RPC/internode auth must fail closed instead of silently using public defaults.
|
||||||
|
- `GHSA-923g-jp7v-f97f` triage: license verification embedded a production RSA private key and used private-key decryption as authenticity. Lesson: ship verifying/public keys only and use real signature verification.
|
||||||
|
|
||||||
|
### Sensitive logging and debug output
|
||||||
|
|
||||||
|
- `GHSA-r54g-49rx-98cr` published: STS credentials were logged at info level. Lesson: generated credentials must never be logged in plaintext.
|
||||||
|
- `GHSA-8cm2-h255-v749` triage: debug logs leaked session tokens, secret keys, JWT claims, and raw STS response bodies. Lesson: redaction must cover custom `Debug` implementations and dependency response-body logging.
|
||||||
|
- `GHSA-333v-68xh-8mmq` published: invalid RPC signature logging included the shared HMAC secret and expected signature. Lesson: error paths often leak secrets; never log raw secrets or derived authenticators.
|
||||||
|
|
||||||
|
### RPC input validation and panic safety
|
||||||
|
|
||||||
|
- `GHSA-gw2x-q739-qhcr` published: malformed gRPC `GetMetrics` payloads reached `unwrap()` on deserialization and caused remote DoS. Lesson: every network/RPC deserialization failure returns an error, not a panic.
|
||||||
|
- `GHSA-h956-rh7x-ppgj` published and `GHSA-r5qv-rc46-hv8q` triage: weak RPC auth increased reachability of otherwise internal handlers. Lesson: panic bugs become more severe when internode auth is weak or defaulted.
|
||||||
|
|
||||||
|
### Browser, CORS, and console isolation
|
||||||
|
|
||||||
|
- `GHSA-v9fg-3cr2-277j` published: object preview rendered attacker-controlled HTML in a same-origin iframe, exposing console credentials stored in `localStorage`. Lesson: user content must be origin-isolated from the console and protected with `nosniff`, CSP, and strict content-type handling.
|
||||||
|
- `GHSA-x5xv-223c-8vm7` triage: default CORS reflected arbitrary origins with credentials. Lesson: never combine reflected origins with `Access-Control-Allow-Credentials: true`; default should be fail-closed.
|
||||||
|
|
||||||
|
### Trusted proxy and source IP conditions
|
||||||
|
|
||||||
|
- `GHSA-fc6g-2gcp-2qrq` published: `aws:SourceIp` trusted client-supplied `X-Forwarded-For` or `X-Real-IP`. Lesson: forwarded IP headers are valid only behind configured trusted proxies; direct clients use socket peer IP.
|
||||||
|
|
||||||
|
### SSE and on-disk storage invariants
|
||||||
|
|
||||||
|
- `GHSA-xrrf-67jm-3c2r` closed: SSE metadata reported encryption while reader composition bypassed `EncryptReader` and stored plaintext. Lesson: test actual bytes on disk and wrapper order, not only API metadata.
|
||||||
|
|
||||||
|
## Useful Search Seeds
|
||||||
|
|
||||||
|
Use these targeted searches when a diff touches security-sensitive code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "validate_admin_request|check_permissions|AdminAction::|deny_only|is_allowed" rustfs crates
|
||||||
|
rg -n "UploadPartCopy|upload_part_copy|CompleteMultipart|PostObject|content-length-range|starts-with" rustfs crates
|
||||||
|
rg -n "PathBuf::join|canonicalize|\\.\\.|x-forwarded-for|x-real-ip|SourceIp" rustfs crates
|
||||||
|
rg -n "DEFAULT_SECRET|DEFAULT_ACCESS|TEST_PRIVATE_KEY|rustfs rpc|RUSTFS_RPC_SECRET" rustfs crates
|
||||||
|
rg -n "debug!|trace!|info!|error!|\\?resp|\\?merged_config|session_token|secret_key" rustfs crates
|
||||||
|
rg -n "HashReader|EncryptReader|SSE|server-side encryption|Access-Control-Allow-Credentials|Origin" rustfs crates
|
||||||
|
```
|
||||||
|
|
||||||
|
## Minimum Regression Test Expectations
|
||||||
|
|
||||||
|
- Authz fixes: include unauthenticated, valid low-privilege, wrong-action, correct-action, owner, non-owner, and root/admin cases as applicable.
|
||||||
|
- IAM fixes: include import/update/list service-account cases with attacker-controlled parent, claims, access key, secret key, and policy.
|
||||||
|
- Copy/upload fixes: include cross-bucket, cross-user, source-denied, destination-denied, copy-source-condition, and multipart completion cases.
|
||||||
|
- Path fixes: include encoded traversal, absolute path, nested traversal, valid object keys that resemble traversal text but should be rejected, and canonical boundary checks.
|
||||||
|
- Logging fixes: assert redacted output for structs and response bodies that may contain credentials.
|
||||||
|
- Browser/CORS fixes: assert no credentials on reflected/default origins, correct behavior for explicit allowlists, and no same-origin script execution for previewed object content.
|
||||||
|
- SSE fixes: inspect stored bytes and verify API metadata, read-back behavior, and on-disk ciphertext together.
|
||||||
@@ -36,6 +36,24 @@ To start a 4-node cluster for distributed testing:
|
|||||||
docker compose -f .docker/compose/docker-compose.cluster.yaml up -d
|
docker compose -f .docker/compose/docker-compose.cluster.yaml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Script-Based 4-Node Validation (Recommended)
|
||||||
|
|
||||||
|
Use the local validation script when you need local-source image build, failover checks,
|
||||||
|
and benchmark workflow in one command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Default mode: WAIT_PROBE_MODE=service
|
||||||
|
# This avoids false negatives where /health/ready remains 503 locally
|
||||||
|
# while the service path is already available.
|
||||||
|
./scripts/run_four_node_cluster_failover_bench.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Strict mode is available when you explicitly want `/health/ready == 200` as the gate:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
WAIT_PROBE_MODE=ready ./scripts/run_four_node_cluster_failover_bench.sh
|
||||||
|
```
|
||||||
|
|
||||||
### (Deprecated) Minimal Observability
|
### (Deprecated) Minimal Observability
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
# Copyright 2024 RustFS Team
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
services:
|
||||||
|
node1:
|
||||||
|
image: ${RUSTFS_IMAGE:-rustfs/rustfs:local-4node}
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: Dockerfile.source
|
||||||
|
hostname: node1
|
||||||
|
environment:
|
||||||
|
- RUSTFS_VOLUMES=http://node{1...4}:9000/data/rustfs{0...3}
|
||||||
|
- RUSTFS_ADDRESS=:9000
|
||||||
|
- RUSTFS_CONSOLE_ENABLE=true
|
||||||
|
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin}
|
||||||
|
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfsadmin}
|
||||||
|
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
|
||||||
|
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
|
||||||
|
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
volumes:
|
||||||
|
- node1_data_0:/data/rustfs0
|
||||||
|
- node1_data_1:/data/rustfs1
|
||||||
|
- node1_data_2:/data/rustfs2
|
||||||
|
- node1_data_3:/data/rustfs3
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
networks:
|
||||||
|
- rustfs-cluster-net
|
||||||
|
|
||||||
|
node2:
|
||||||
|
image: ${RUSTFS_IMAGE:-rustfs/rustfs:local-4node}
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: Dockerfile.source
|
||||||
|
hostname: node2
|
||||||
|
environment:
|
||||||
|
- RUSTFS_VOLUMES=http://node{1...4}:9000/data/rustfs{0...3}
|
||||||
|
- RUSTFS_ADDRESS=:9000
|
||||||
|
- RUSTFS_CONSOLE_ENABLE=true
|
||||||
|
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin}
|
||||||
|
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfsadmin}
|
||||||
|
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
|
||||||
|
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
|
||||||
|
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
volumes:
|
||||||
|
- node2_data_0:/data/rustfs0
|
||||||
|
- node2_data_1:/data/rustfs1
|
||||||
|
- node2_data_2:/data/rustfs2
|
||||||
|
- node2_data_3:/data/rustfs3
|
||||||
|
ports:
|
||||||
|
- "9001:9000"
|
||||||
|
networks:
|
||||||
|
- rustfs-cluster-net
|
||||||
|
|
||||||
|
node3:
|
||||||
|
image: ${RUSTFS_IMAGE:-rustfs/rustfs:local-4node}
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: Dockerfile.source
|
||||||
|
hostname: node3
|
||||||
|
environment:
|
||||||
|
- RUSTFS_VOLUMES=http://node{1...4}:9000/data/rustfs{0...3}
|
||||||
|
- RUSTFS_ADDRESS=:9000
|
||||||
|
- RUSTFS_CONSOLE_ENABLE=true
|
||||||
|
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin}
|
||||||
|
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfsadmin}
|
||||||
|
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
|
||||||
|
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
|
||||||
|
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
volumes:
|
||||||
|
- node3_data_0:/data/rustfs0
|
||||||
|
- node3_data_1:/data/rustfs1
|
||||||
|
- node3_data_2:/data/rustfs2
|
||||||
|
- node3_data_3:/data/rustfs3
|
||||||
|
ports:
|
||||||
|
- "9002:9000"
|
||||||
|
networks:
|
||||||
|
- rustfs-cluster-net
|
||||||
|
|
||||||
|
node4:
|
||||||
|
image: ${RUSTFS_IMAGE:-rustfs/rustfs:local-4node}
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: Dockerfile.source
|
||||||
|
hostname: node4
|
||||||
|
environment:
|
||||||
|
- RUSTFS_VOLUMES=http://node{1...4}:9000/data/rustfs{0...3}
|
||||||
|
- RUSTFS_ADDRESS=:9000
|
||||||
|
- RUSTFS_CONSOLE_ENABLE=true
|
||||||
|
- RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin}
|
||||||
|
- RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfsadmin}
|
||||||
|
- RUSTFS_OBS_ENDPOINT=${RUSTFS_OBS_ENDPOINT:-http://host.docker.internal:4318}
|
||||||
|
- RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL:-info}
|
||||||
|
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=${RUSTFS_UNSAFE_BYPASS_DISK_CHECK:-true}
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
volumes:
|
||||||
|
- node4_data_0:/data/rustfs0
|
||||||
|
- node4_data_1:/data/rustfs1
|
||||||
|
- node4_data_2:/data/rustfs2
|
||||||
|
- node4_data_3:/data/rustfs3
|
||||||
|
ports:
|
||||||
|
- "9003:9000"
|
||||||
|
networks:
|
||||||
|
- rustfs-cluster-net
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
node1_data_0:
|
||||||
|
node1_data_1:
|
||||||
|
node1_data_2:
|
||||||
|
node1_data_3:
|
||||||
|
node2_data_0:
|
||||||
|
node2_data_1:
|
||||||
|
node2_data_2:
|
||||||
|
node2_data_3:
|
||||||
|
node3_data_0:
|
||||||
|
node3_data_1:
|
||||||
|
node3_data_2:
|
||||||
|
node3_data_3:
|
||||||
|
node4_data_0:
|
||||||
|
node4_data_1:
|
||||||
|
node4_data_2:
|
||||||
|
node4_data_3:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
rustfs-cluster-net:
|
||||||
|
driver: bridge
|
||||||
@@ -26,7 +26,7 @@ services:
|
|||||||
restart: "no"
|
restart: "no"
|
||||||
|
|
||||||
tempo:
|
tempo:
|
||||||
image: grafana/tempo:latest
|
image: grafana/tempo:2.10.5
|
||||||
user: "10001"
|
user: "10001"
|
||||||
command: [ "-config.file=/etc/tempo.yaml" ]
|
command: [ "-config.file=/etc/tempo.yaml" ]
|
||||||
volumes:
|
volumes:
|
||||||
@@ -36,9 +36,19 @@ services:
|
|||||||
- "3200:3200" # tempo
|
- "3200:3200" # tempo
|
||||||
- "4317" # otlp grpc
|
- "4317" # otlp grpc
|
||||||
- "4318" # otlp http
|
- "4318" # otlp http
|
||||||
|
- "7946" # memberlist
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- rustfs-network
|
- rustfs-network
|
||||||
|
depends_on:
|
||||||
|
tempo-init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "/tempo", "-version" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: otel/opentelemetry-collector-contrib:latest
|
image: otel/opentelemetry-collector-contrib:latest
|
||||||
@@ -61,6 +71,12 @@ services:
|
|||||||
- jaeger
|
- jaeger
|
||||||
- prometheus
|
- prometheus
|
||||||
- loki
|
- loki
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "/otelcol-contrib", "--version" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
jaeger:
|
jaeger:
|
||||||
image: jaegertracing/jaeger:latest
|
image: jaegertracing/jaeger:latest
|
||||||
@@ -72,12 +88,23 @@ services:
|
|||||||
- BADGER_DIRECTORY_KEY=/badger/key
|
- BADGER_DIRECTORY_KEY=/badger/key
|
||||||
- COLLECTOR_OTLP_ENABLED=true
|
- COLLECTOR_OTLP_ENABLED=true
|
||||||
volumes:
|
volumes:
|
||||||
|
- ../../.docker/observability/jaeger.yaml:/etc/jaeger/config.yml:ro
|
||||||
- jaeger-data:/badger
|
- jaeger-data:/badger
|
||||||
ports:
|
ports:
|
||||||
- "16686:16686" # Web UI
|
- "16686:16686" # Web UI
|
||||||
- "14269:14269" # Admin/Metrics
|
- "14269:14269" # Admin/Metrics
|
||||||
|
- "4317" # otlp grpc
|
||||||
|
- "4318" # otlp http
|
||||||
|
command: [ "--config", "/etc/jaeger/config.yml" ]
|
||||||
networks:
|
networks:
|
||||||
- rustfs-network
|
- rustfs-network
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:14269" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
|
||||||
prometheus:
|
prometheus:
|
||||||
image: prom/prometheus:latest
|
image: prom/prometheus:latest
|
||||||
@@ -85,6 +112,7 @@ services:
|
|||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
volumes:
|
volumes:
|
||||||
- ../../.docker/observability/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
- ../../.docker/observability/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||||
|
- ../../.docker/observability/prometheus-rules:/etc/prometheus/rules:ro
|
||||||
- prometheus-data:/prometheus
|
- prometheus-data:/prometheus
|
||||||
ports:
|
ports:
|
||||||
- "9090:9090"
|
- "9090:9090"
|
||||||
@@ -94,23 +122,45 @@ services:
|
|||||||
- '--web.enable-remote-write-receiver'
|
- '--web.enable-remote-write-receiver'
|
||||||
- '--enable-feature=promql-experimental-functions'
|
- '--enable-feature=promql-experimental-functions'
|
||||||
- '--storage.tsdb.path=/prometheus'
|
- '--storage.tsdb.path=/prometheus'
|
||||||
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
|
- '--storage.tsdb.retention.time=30d'
|
||||||
- '--web.console.templates=/usr/share/prometheus/consoles'
|
|
||||||
networks:
|
networks:
|
||||||
- rustfs-network
|
- rustfs-network
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:9090/-/healthy" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
loki:
|
loki:
|
||||||
image: grafana/loki:latest
|
image: grafana/loki:latest
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
volumes:
|
volumes:
|
||||||
- ../../.docker/observability/loki.yaml:/etc/loki/local-config.yaml:ro
|
- ../../.docker/observability/loki.yaml:/etc/loki/loki.yaml:ro
|
||||||
- loki-data:/loki
|
- loki-data:/loki
|
||||||
ports:
|
ports:
|
||||||
- "3100:3100"
|
- "3100:3100"
|
||||||
command: -config.file=/etc/loki/local-config.yaml
|
command: -config.file=/etc/loki/loki.yaml
|
||||||
networks:
|
networks:
|
||||||
- rustfs-network
|
- rustfs-network
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:3100/ready" ]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 60s
|
||||||
|
|
||||||
|
pyroscope:
|
||||||
|
image: grafana/pyroscope:latest
|
||||||
|
ports:
|
||||||
|
- "4040:4040"
|
||||||
|
command:
|
||||||
|
- -self-profiling.disable-push=true
|
||||||
|
networks:
|
||||||
|
- rustfs-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
grafana:
|
grafana:
|
||||||
image: grafana/grafana:latest
|
image: grafana/grafana:latest
|
||||||
@@ -127,10 +177,17 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ../../.docker/observability/grafana/provisioning:/etc/grafana/provisioning:ro
|
- ../../.docker/observability/grafana/provisioning:/etc/grafana/provisioning:ro
|
||||||
- ../../.docker/observability/grafana/dashboards:/var/lib/grafana/dashboards:ro
|
- ../../.docker/observability/grafana/dashboards:/var/lib/grafana/dashboards:ro
|
||||||
|
- grafana-data:/var/lib/grafana
|
||||||
depends_on:
|
depends_on:
|
||||||
- prometheus
|
- prometheus
|
||||||
- tempo
|
- tempo
|
||||||
- loki
|
- loki
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:3000/api/health" ]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
# --- RustFS Cluster ---
|
# --- RustFS Cluster ---
|
||||||
|
|
||||||
@@ -215,6 +272,7 @@ volumes:
|
|||||||
tempo-data:
|
tempo-data:
|
||||||
loki-data:
|
loki-data:
|
||||||
jaeger-data:
|
jaeger-data:
|
||||||
|
grafana-data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
rustfs-network:
|
rustfs-network:
|
||||||
|
|||||||
@@ -74,6 +74,17 @@ http {
|
|||||||
# ssl_certificate /etc/nginx/ssl/server.crt;
|
# ssl_certificate /etc/nginx/ssl/server.crt;
|
||||||
# ssl_certificate_key /etc/nginx/ssl/server.key;
|
# ssl_certificate_key /etc/nginx/ssl/server.key;
|
||||||
#
|
#
|
||||||
|
# # Restrict to modern TLS versions and ciphers. Operators copying this
|
||||||
|
# # example must keep at least these directives — without them, nginx
|
||||||
|
# # may negotiate older protocol versions that have known weaknesses.
|
||||||
|
# ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
# ssl_prefer_server_ciphers on;
|
||||||
|
# ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
|
||||||
|
# ssl_session_timeout 1d;
|
||||||
|
# ssl_session_cache shared:SSL:10m;
|
||||||
|
# ssl_session_tickets off;
|
||||||
|
# # add_header Strict-Transport-Security "max-age=63072000" always;
|
||||||
|
#
|
||||||
# location / {
|
# location / {
|
||||||
# proxy_pass http://rustfs:9000;
|
# proxy_pass http://rustfs:9000;
|
||||||
# ...
|
# ...
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ The stack is composed of the following best-in-class open-source components:
|
|||||||
- **Jaeger** (v1.59.0): Distributed tracing system (configured as a secondary UI/storage).
|
- **Jaeger** (v1.59.0): Distributed tracing system (configured as a secondary UI/storage).
|
||||||
- **OpenTelemetry Collector** (v0.104.0): A vendor-agnostic implementation for receiving, processing, and exporting telemetry data.
|
- **OpenTelemetry Collector** (v0.104.0): A vendor-agnostic implementation for receiving, processing, and exporting telemetry data.
|
||||||
|
|
||||||
|
By default, this stack uses Tempo in single-binary mode and does not require Kafka/Redpanda.
|
||||||
|
If you want the Kafka-backed HA Tempo path, use `docker-compose-example-for-rustfs.yml` together with `docker-compose-tempo-ha-override.yml`.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
1. **Telemetry Collection**: Applications send OTLP (OpenTelemetry Protocol) data (Metrics, Logs, Traces) to the **OpenTelemetry Collector**.
|
1. **Telemetry Collection**: Applications send OTLP (OpenTelemetry Protocol) data (Metrics, Logs, Traces) to the **OpenTelemetry Collector**.
|
||||||
@@ -46,6 +49,15 @@ Run the following command to start the entire stack:
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### High Availability Tempo
|
||||||
|
|
||||||
|
The default `docker-compose.yml` is the single-node stack.
|
||||||
|
If you need the Kafka-backed HA Tempo configuration, start it with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose-example-for-rustfs.yml -f docker-compose-tempo-ha-override.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
### Access Dashboards
|
### Access Dashboards
|
||||||
|
|
||||||
| Service | URL | Credentials | Description |
|
| Service | URL | Credentials | Description |
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
- **Jaeger** (v1.59.0): 分布式追踪系统(配置为辅助 UI/存储)。
|
- **Jaeger** (v1.59.0): 分布式追踪系统(配置为辅助 UI/存储)。
|
||||||
- **OpenTelemetry Collector** (v0.104.0): 接收、处理和导出遥测数据的供应商无关实现。
|
- **OpenTelemetry Collector** (v0.104.0): 接收、处理和导出遥测数据的供应商无关实现。
|
||||||
|
|
||||||
|
默认情况下,这套技术栈使用 Tempo 单二进制模式,不依赖 Kafka/Redpanda。
|
||||||
|
如果需要基于 Kafka 的 HA Tempo 路径,请使用 `docker-compose-example-for-rustfs.yml` 配合 `docker-compose-tempo-ha-override.yml`。
|
||||||
|
|
||||||
## 架构
|
## 架构
|
||||||
|
|
||||||
1. **遥测收集**: 应用程序将 OTLP (OpenTelemetry Protocol) 数据(指标、日志、追踪)发送到 **OpenTelemetry Collector**。
|
1. **遥测收集**: 应用程序将 OTLP (OpenTelemetry Protocol) 数据(指标、日志、追踪)发送到 **OpenTelemetry Collector**。
|
||||||
@@ -46,6 +49,15 @@
|
|||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Tempo 高可用模式
|
||||||
|
|
||||||
|
默认的 `docker-compose.yml` 对应单机栈。
|
||||||
|
如果需要基于 Kafka 的 HA Tempo 配置,请使用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose-example-for-rustfs.yml -f docker-compose-tempo-ha-override.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
### 访问仪表盘
|
### 访问仪表盘
|
||||||
|
|
||||||
| 服务 | URL | 凭据 | 描述 |
|
| 服务 | URL | 凭据 | 描述 |
|
||||||
|
|||||||
@@ -85,10 +85,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- otel-network
|
- otel-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
|
||||||
- redpanda
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:3200/ready" ]
|
test: [ "CMD", "/tempo", "-version" ]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
@@ -205,7 +203,7 @@ services:
|
|||||||
- prometheus
|
- prometheus
|
||||||
- loki
|
- loki
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:13133" ]
|
test: [ "CMD", "/otelcol-contrib", "--version" ]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@@ -16,10 +16,8 @@ services:
|
|||||||
|
|
||||||
# --- Tracing ---
|
# --- Tracing ---
|
||||||
tempo:
|
tempo:
|
||||||
image: grafana/tempo:2.10.3
|
image: grafana/tempo:2.10.5
|
||||||
container_name: tempo
|
container_name: tempo
|
||||||
depends_on:
|
|
||||||
- redpanda
|
|
||||||
command: [ "-config.file=/etc/tempo.yaml" ]
|
command: [ "-config.file=/etc/tempo.yaml" ]
|
||||||
volumes:
|
volumes:
|
||||||
- ./tempo.yaml:/etc/tempo.yaml:ro
|
- ./tempo.yaml:/etc/tempo.yaml:ro
|
||||||
@@ -33,31 +31,11 @@ services:
|
|||||||
- otel-network
|
- otel-network
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:3200/ready" ]
|
test: [ "CMD", "/tempo", "-version" ]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 15s
|
start_period: 15s
|
||||||
redpanda:
|
|
||||||
image: redpandadata/redpanda:latest
|
|
||||||
ports:
|
|
||||||
- "9092:9092" # Kafka API for clients
|
|
||||||
command: >
|
|
||||||
redpanda start --overprovisioned
|
|
||||||
--mode=dev-container
|
|
||||||
--kafka-addr=PLAINTEXT://0.0.0.0:9092
|
|
||||||
--advertise-kafka-addr=PLAINTEXT://redpanda:9092
|
|
||||||
|
|
||||||
redpanda-console:
|
|
||||||
image: docker.redpanda.com/redpandadata/console:latest
|
|
||||||
environment:
|
|
||||||
- CONFIG_FILEPATH=/etc/redpanda/redpanda-console-config.yaml
|
|
||||||
volumes:
|
|
||||||
- ./redpanda-console.yaml:/etc/redpanda/redpanda-console-config.yaml
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
depends_on:
|
|
||||||
- redpanda
|
|
||||||
|
|
||||||
vulture:
|
vulture:
|
||||||
image: grafana/tempo-vulture:latest
|
image: grafana/tempo-vulture:latest
|
||||||
@@ -106,6 +84,7 @@ services:
|
|||||||
container_name: prometheus
|
container_name: prometheus
|
||||||
volumes:
|
volumes:
|
||||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||||
|
- ./prometheus-rules:/etc/prometheus/rules:ro
|
||||||
- prometheus-data:/prometheus
|
- prometheus-data:/prometheus
|
||||||
ports:
|
ports:
|
||||||
- "9090:9090"
|
- "9090:9090"
|
||||||
@@ -168,7 +147,7 @@ services:
|
|||||||
- prometheus
|
- prometheus
|
||||||
- loki
|
- loki
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD", "wget", "--spider", "-q", "http://localhost:13133" ]
|
test: [ "CMD", "/otelcol-contrib", "--version" ]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,97 +0,0 @@
|
|||||||
# Copyright 2024 RustFS Team
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
apiVersion: 1
|
|
||||||
|
|
||||||
datasources:
|
|
||||||
- name: Prometheus
|
|
||||||
type: prometheus
|
|
||||||
uid: prometheus
|
|
||||||
access: proxy
|
|
||||||
orgId: 1
|
|
||||||
url: http://prometheus:9090
|
|
||||||
isDefault: true
|
|
||||||
version: 1
|
|
||||||
editable: false
|
|
||||||
jsonData:
|
|
||||||
httpMethod: GET
|
|
||||||
exemplarTraceIdDestinations:
|
|
||||||
- name: trace_id
|
|
||||||
datasourceUid: tempo
|
|
||||||
|
|
||||||
- name: Tempo
|
|
||||||
type: tempo
|
|
||||||
uid: tempo
|
|
||||||
access: proxy
|
|
||||||
orgId: 1
|
|
||||||
url: http://tempo:3200
|
|
||||||
isDefault: false
|
|
||||||
version: 1
|
|
||||||
editable: false
|
|
||||||
jsonData:
|
|
||||||
httpMethod: GET
|
|
||||||
serviceMap:
|
|
||||||
datasourceUid: prometheus
|
|
||||||
tracesToLogs:
|
|
||||||
datasourceUid: loki
|
|
||||||
tags: [ 'job', 'instance', 'pod', 'namespace', 'service.name' ]
|
|
||||||
mappedTags: [ { key: 'service.name', value: 'app' } ]
|
|
||||||
spanStartTimeShift: '1s'
|
|
||||||
spanEndTimeShift: '-1s'
|
|
||||||
filterByTraceID: true
|
|
||||||
filterBySpanID: false
|
|
||||||
tracesToMetrics:
|
|
||||||
datasourceUid: prometheus
|
|
||||||
tags: [ { key: 'service.name' }, { key: 'job' } ]
|
|
||||||
queries:
|
|
||||||
- name: 'Service-Level Latency'
|
|
||||||
query: 'sum(rate(traces_spanmetrics_latency_bucket{$$__tags}[5m])) by (le)'
|
|
||||||
- name: 'Service-Level Calls'
|
|
||||||
query: 'sum(rate(traces_spanmetrics_calls_total{$$__tags}[5m]))'
|
|
||||||
- name: 'Service-Level Errors'
|
|
||||||
query: 'sum(rate(traces_spanmetrics_calls_total{status_code="ERROR", $$__tags}[5m]))'
|
|
||||||
nodeGraph:
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
- name: Loki
|
|
||||||
type: loki
|
|
||||||
uid: loki
|
|
||||||
orgId: 1
|
|
||||||
url: http://loki:3100
|
|
||||||
isDefault: false
|
|
||||||
version: 1
|
|
||||||
editable: false
|
|
||||||
jsonData:
|
|
||||||
derivedFields:
|
|
||||||
- datasourceUid: tempo
|
|
||||||
matcherRegex: 'trace_id=(\w+)'
|
|
||||||
name: 'TraceID'
|
|
||||||
url: '$${__value.raw}'
|
|
||||||
|
|
||||||
- name: Jaeger
|
|
||||||
type: jaeger
|
|
||||||
uid: jaeger
|
|
||||||
url: http://jaeger:16686
|
|
||||||
access: proxy
|
|
||||||
isDefault: false
|
|
||||||
editable: false
|
|
||||||
jsonData:
|
|
||||||
tracesToLogs:
|
|
||||||
datasourceUid: loki
|
|
||||||
tags: [ 'job', 'instance', 'pod', 'namespace', 'service.name' ]
|
|
||||||
mappedTags: [ { key: 'service.name', value: 'app' } ]
|
|
||||||
spanStartTimeShift: '1s'
|
|
||||||
spanEndTimeShift: '-1s'
|
|
||||||
filterByTraceID: true
|
|
||||||
filterBySpanID: false
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
groups:
|
||||||
|
- name: rustfs-dashboard
|
||||||
|
interval: 30s
|
||||||
|
rules:
|
||||||
|
- record: rustfs:http_server_requests:rate5m
|
||||||
|
expr: sum by (job) (rate(rustfs_http_server_requests_total[5m]))
|
||||||
|
|
||||||
|
- record: rustfs:http_server_request_duration_seconds:p50_5m
|
||||||
|
expr: histogram_quantile(0.50, sum by (le, job) (rate(rustfs_http_server_request_duration_seconds_bucket[5m])))
|
||||||
|
- record: rustfs:http_server_request_duration_seconds:p95_5m
|
||||||
|
expr: histogram_quantile(0.95, sum by (le, job) (rate(rustfs_http_server_request_duration_seconds_bucket[5m])))
|
||||||
|
- record: rustfs:http_server_request_duration_seconds:p99_5m
|
||||||
|
expr: histogram_quantile(0.99, sum by (le, job) (rate(rustfs_http_server_request_duration_seconds_bucket[5m])))
|
||||||
|
|
||||||
|
- record: rustfs:http_server_response_body_size_bytes:p50_5m
|
||||||
|
expr: histogram_quantile(0.50, sum by (le, job) (rate(rustfs_http_server_response_body_size_bytes_bucket[5m])))
|
||||||
|
- record: rustfs:http_server_response_body_size_bytes:p95_5m
|
||||||
|
expr: histogram_quantile(0.95, sum by (le, job) (rate(rustfs_http_server_response_body_size_bytes_bucket[5m])))
|
||||||
|
- record: rustfs:http_server_response_body_size_bytes:p99_5m
|
||||||
|
expr: histogram_quantile(0.99, sum by (le, job) (rate(rustfs_http_server_response_body_size_bytes_bucket[5m])))
|
||||||
|
|
||||||
|
- record: rustfs:log_cleaner_runs:rate15m
|
||||||
|
expr: sum by (job) (rate(rustfs_log_cleaner_runs_total[15m]))
|
||||||
|
- record: rustfs:log_cleaner_failure_ratio:rate5m
|
||||||
|
expr: sum by (job) (rate(rustfs_log_cleaner_run_failures_total[5m])) / clamp_min(sum by (job) (rate(rustfs_log_cleaner_runs_total[5m])), 1e-9)
|
||||||
|
- record: rustfs:log_cleaner_rotation_failure_ratio:rate5m
|
||||||
|
expr: sum by (job) (rate(rustfs_log_cleaner_rotation_failures_total[5m])) / clamp_min(sum by (job) (rate(rustfs_log_cleaner_rotation_total[5m])), 1e-9)
|
||||||
|
- record: rustfs:log_cleaner_rotation_duration_seconds:p95_5m
|
||||||
|
expr: histogram_quantile(0.95, sum by (le, job) (rate(rustfs_log_cleaner_rotation_duration_seconds_bucket[5m])))
|
||||||
|
- record: rustfs:log_cleaner_compress_duration_seconds:p95_5m
|
||||||
|
expr: histogram_quantile(0.95, sum by (le, job) (rate(rustfs_log_cleaner_compress_duration_seconds_bucket[5m])))
|
||||||
|
|
||||||
|
- record: rustfs:scanner_objects_scanned:rate5m
|
||||||
|
expr: sum by (job) (rate(rustfs_scanner_objects_scanned_total[5m]))
|
||||||
|
- record: rustfs:scanner_directories_scanned:rate5m
|
||||||
|
expr: sum by (job) (rate(rustfs_scanner_directories_scanned_total[5m]))
|
||||||
|
- record: rustfs:scanner_buckets_scanned:rate5m
|
||||||
|
expr: sum by (job) (rate(rustfs_scanner_buckets_scanned_total[5m]))
|
||||||
|
- record: rustfs:scanner_cycles_success:rate5m
|
||||||
|
expr: sum by (job) (rate(rustfs_scanner_cycles_total{result="success"}[5m]))
|
||||||
@@ -19,6 +19,9 @@ global:
|
|||||||
cluster: 'rustfs-dev' # Label to identify the cluster
|
cluster: 'rustfs-dev' # Label to identify the cluster
|
||||||
replica: '1' # Replica identifier
|
replica: '1' # Replica identifier
|
||||||
|
|
||||||
|
rule_files:
|
||||||
|
- /etc/prometheus/rules/*.yml
|
||||||
|
|
||||||
scrape_configs:
|
scrape_configs:
|
||||||
- job_name: 'otel-collector'
|
- job_name: 'otel-collector'
|
||||||
static_configs:
|
static_configs:
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
partition_ring_live_store: true
|
|
||||||
stream_over_http_enabled: true
|
stream_over_http_enabled: true
|
||||||
|
|
||||||
server:
|
server:
|
||||||
@@ -33,33 +32,17 @@ distributor:
|
|||||||
endpoint: "0.0.0.0:4317"
|
endpoint: "0.0.0.0:4317"
|
||||||
http:
|
http:
|
||||||
endpoint: "0.0.0.0:4318"
|
endpoint: "0.0.0.0:4318"
|
||||||
#log_received_spans:
|
|
||||||
# enabled: true
|
|
||||||
# log_discarded_spans:
|
|
||||||
# enabled: true
|
|
||||||
|
|
||||||
backend_scheduler:
|
ingester:
|
||||||
provider:
|
max_block_duration: 5m
|
||||||
compaction:
|
|
||||||
compaction:
|
|
||||||
block_retention: 1h
|
|
||||||
|
|
||||||
backend_worker:
|
|
||||||
backend_scheduler_addr: localhost:3200
|
|
||||||
compaction:
|
|
||||||
block_retention: 1h
|
|
||||||
ring:
|
|
||||||
kvstore:
|
|
||||||
store: memberlist
|
|
||||||
|
|
||||||
querier:
|
|
||||||
query_live_store: true
|
|
||||||
|
|
||||||
metrics_generator:
|
metrics_generator:
|
||||||
registry:
|
registry:
|
||||||
external_labels:
|
external_labels:
|
||||||
source: tempo
|
source: tempo
|
||||||
cluster: docker-compose
|
cluster: docker-compose
|
||||||
|
traces_storage:
|
||||||
|
path: /var/tempo/generator/traces
|
||||||
storage:
|
storage:
|
||||||
path: /var/tempo/generator/wal
|
path: /var/tempo/generator/wal
|
||||||
remote_write:
|
remote_write:
|
||||||
@@ -85,15 +68,5 @@ overrides:
|
|||||||
processors: [ "span-metrics", "service-graphs", "local-blocks" ]
|
processors: [ "span-metrics", "service-graphs", "local-blocks" ]
|
||||||
generate_native_histograms: both
|
generate_native_histograms: both
|
||||||
|
|
||||||
ingest:
|
|
||||||
enabled: true
|
|
||||||
kafka:
|
|
||||||
address: redpanda:9092
|
|
||||||
topic: tempo-ingest
|
|
||||||
|
|
||||||
block_builder:
|
|
||||||
consume_cycle_duration: 30s
|
|
||||||
|
|
||||||
usage_report:
|
usage_report:
|
||||||
reporting_enabled: false
|
reporting_enabled: false
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
services:
|
||||||
|
rustfs:
|
||||||
|
image: rustfs/rustfs:1.0.0-alpha.99-glibc
|
||||||
|
container_name: rustfs-issue-2715-test
|
||||||
|
security_opt:
|
||||||
|
- "no-new-privileges:true"
|
||||||
|
ports:
|
||||||
|
- "19000:9000"
|
||||||
|
- "19001:9001"
|
||||||
|
environment:
|
||||||
|
- RUSTFS_VOLUMES=/data/rustfs{0...8}
|
||||||
|
- RUSTFS_ADDRESS=0.0.0.0:9000
|
||||||
|
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
|
||||||
|
- RUSTFS_CONSOLE_ENABLE=true
|
||||||
|
- RUSTFS_CORS_ALLOWED_ORIGINS=*
|
||||||
|
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
|
||||||
|
- RUSTFS_ACCESS_KEY=admin
|
||||||
|
- RUSTFS_SECRET_KEY=admin
|
||||||
|
- RUSTFS_OBS_LOGGER_LEVEL=info
|
||||||
|
- RUSTFS_OBS_ENDPOINT=http://otel-collector:4318
|
||||||
|
- RUSTFS_OBS_PROFILING_ENDPOINT=http://pyroscope:4040
|
||||||
|
- RUSTFS_STORAGE_CLASS_STANDARD=EC:2
|
||||||
|
- RUSTFS_STORAGE_CLASS_RRS=EC:1
|
||||||
|
- RUSTFS_UNSAFE_BYPASS_DISK_CHECK=true
|
||||||
|
- RUSTFS_OBS_LOG_DIRECTORY=/opt/rustfs/logs
|
||||||
|
extra_hosts:
|
||||||
|
- "otel-collector:host-gateway"
|
||||||
|
- "pyroscope:host-gateway"
|
||||||
|
volumes:
|
||||||
|
- ./deploy/data/issue-2715/rustfs0:/data/rustfs0
|
||||||
|
- ./deploy/data/issue-2715/rustfs1:/data/rustfs1
|
||||||
|
- ./deploy/data/issue-2715/rustfs2:/data/rustfs2
|
||||||
|
- ./deploy/data/issue-2715/rustfs3:/data/rustfs3
|
||||||
|
- ./deploy/data/issue-2715/rustfs4:/data/rustfs4
|
||||||
|
- ./deploy/data/issue-2715/rustfs5:/data/rustfs5
|
||||||
|
- ./deploy/data/issue-2715/rustfs6:/data/rustfs6
|
||||||
|
- ./deploy/data/issue-2715/rustfs7:/data/rustfs7
|
||||||
|
- ./deploy/data/issue-2715/rustfs8:/data/rustfs8
|
||||||
|
- ./deploy/logs/issue-2715:/opt/rustfs/logs
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"sh",
|
||||||
|
"-c",
|
||||||
|
"curl -f http://127.0.0.1:9000/health && curl -f http://127.0.0.1:9001/rustfs/console/health"
|
||||||
|
]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
data/
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Issue 2815 Local Docker Verification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This directory contains the local distributed Docker verification assets used to validate issue `#2815` against the current source build.
|
||||||
|
|
||||||
|
The target behavior is:
|
||||||
|
|
||||||
|
- 4-node distributed cluster starts successfully
|
||||||
|
- `/health/ready` becomes reachable on each node
|
||||||
|
- logs no longer contain `storage_info failed: Io error: wrong msgpack marker FixArray(1)`
|
||||||
|
- internode RPC authentication succeeds with an explicit non-default RPC secret
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `docker-compose.yml`: 4-node distributed cluster using a locally built image
|
||||||
|
|
||||||
|
## Data Directories
|
||||||
|
|
||||||
|
Create the bind-mount directories before `docker compose up`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p .docker/test/issues-2815/data/rustfs{1..4}-disk{0..3}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Apple Silicon / arm64 host:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build --platform linux/arm64 -f Dockerfile.source -t rustfs-issue-2815-local .
|
||||||
|
```
|
||||||
|
|
||||||
|
If you intentionally want amd64 emulation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build --platform linux/amd64 -f Dockerfile.source -t rustfs-issue-2815-local .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f .docker/test/issues-2815/docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
If the image platform is not `linux/arm64`, align compose explicitly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
RUSTFS_DOCKER_PLATFORM=linux/amd64 docker compose -f .docker/test/issues-2815/docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Health Checks
|
||||||
|
|
||||||
|
Container-level healthcheck is now included and probes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsS http://127.0.0.1:9000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Manual checks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i http://127.0.0.1:9101/health/ready
|
||||||
|
curl -i http://127.0.0.1:9102/health/ready
|
||||||
|
curl -i http://127.0.0.1:9103/health/ready
|
||||||
|
curl -i http://127.0.0.1:9104/health/ready
|
||||||
|
```
|
||||||
|
|
||||||
|
## RPC Secret Requirement
|
||||||
|
|
||||||
|
The current source build no longer reproduces the original `FixArray(1)` decode error from issue `#2815`.
|
||||||
|
|
||||||
|
Earlier local Docker attempts failed during erasure bootstrap with:
|
||||||
|
|
||||||
|
```text
|
||||||
|
No valid auth token
|
||||||
|
store init failed to load formats after 10 retries: erasure read quorum
|
||||||
|
```
|
||||||
|
|
||||||
|
Root cause:
|
||||||
|
|
||||||
|
- RPC authentication rejects the default secret `rustfsadmin`
|
||||||
|
- distributed local Docker validation therefore needs an explicit non-default secret
|
||||||
|
|
||||||
|
This compose now sets both:
|
||||||
|
|
||||||
|
- `RUSTFS_SECRET_KEY=issue-2815-secret`
|
||||||
|
- `RUSTFS_RPC_SECRET=issue-2815-rpc-secret`
|
||||||
|
|
||||||
|
With those values in place, the current 4-node local Docker cluster reaches healthy state and `/health/ready` returns `200`.
|
||||||
|
|
||||||
|
In other words:
|
||||||
|
|
||||||
|
- `RUSTFS_ACCESS_KEY` may still be `rustfsadmin` for local service credentials if desired
|
||||||
|
- `RUSTFS_SECRET_KEY` can still be used for service credentials
|
||||||
|
- but RPC authentication must not resolve to the default secret value `rustfsadmin`
|
||||||
|
- if `RUSTFS_RPC_SECRET` is unset, the code falls back to `RUSTFS_SECRET_KEY`
|
||||||
|
- so at least one of them must provide a non-default shared secret for internode RPC signing
|
||||||
|
|
||||||
|
## Suggested Debug Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f .docker/test/issues-2815/docker-compose.yml ps
|
||||||
|
docker compose -f .docker/test/issues-2815/docker-compose.yml logs --no-color --tail=200
|
||||||
|
docker compose -f .docker/test/issues-2815/docker-compose.yml down -v
|
||||||
|
```
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
services:
|
||||||
|
rustfs1:
|
||||||
|
image: rustfs-issue-2815-local
|
||||||
|
platform: ${RUSTFS_DOCKER_PLATFORM:-linux/arm64}
|
||||||
|
hostname: rustfs1
|
||||||
|
container_name: rustfs-issue-2815-rustfs1
|
||||||
|
environment:
|
||||||
|
RUSTFS_ADDRESS: "0.0.0.0:9000"
|
||||||
|
RUSTFS_ACCESS_KEY: "rustfsadmin"
|
||||||
|
RUSTFS_SECRET_KEY: "issue-2815-secret"
|
||||||
|
RUSTFS_RPC_SECRET: "issue-2815-rpc-secret"
|
||||||
|
RUSTFS_CONSOLE_ENABLE: "false"
|
||||||
|
RUST_LOG: "info"
|
||||||
|
RUSTFS_UNSAFE_BYPASS_DISK_CHECK: "true"
|
||||||
|
RUSTFS_VOLUMES: "http://rustfs{1...4}:9000/data/rustfs{0...3}"
|
||||||
|
volumes:
|
||||||
|
- ./data/rustfs1-disk0:/data/rustfs0
|
||||||
|
- ./data/rustfs1-disk1:/data/rustfs1
|
||||||
|
- ./data/rustfs1-disk2:/data/rustfs2
|
||||||
|
- ./data/rustfs1-disk3:/data/rustfs3
|
||||||
|
networks: [rustfs-issue-2815-net]
|
||||||
|
ports:
|
||||||
|
- "9101:9000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "sh", "-c", "curl -fsS http://127.0.0.1:9000/health || exit 1"]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 8
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
rustfs2:
|
||||||
|
image: rustfs-issue-2815-local
|
||||||
|
platform: ${RUSTFS_DOCKER_PLATFORM:-linux/arm64}
|
||||||
|
hostname: rustfs2
|
||||||
|
container_name: rustfs-issue-2815-rustfs2
|
||||||
|
environment:
|
||||||
|
RUSTFS_ADDRESS: "0.0.0.0:9000"
|
||||||
|
RUSTFS_ACCESS_KEY: "rustfsadmin"
|
||||||
|
RUSTFS_SECRET_KEY: "issue-2815-secret"
|
||||||
|
RUSTFS_RPC_SECRET: "issue-2815-rpc-secret"
|
||||||
|
RUSTFS_CONSOLE_ENABLE: "false"
|
||||||
|
RUST_LOG: "info"
|
||||||
|
RUSTFS_UNSAFE_BYPASS_DISK_CHECK: "true"
|
||||||
|
RUSTFS_VOLUMES: "http://rustfs{1...4}:9000/data/rustfs{0...3}"
|
||||||
|
volumes:
|
||||||
|
- ./data/rustfs2-disk0:/data/rustfs0
|
||||||
|
- ./data/rustfs2-disk1:/data/rustfs1
|
||||||
|
- ./data/rustfs2-disk2:/data/rustfs2
|
||||||
|
- ./data/rustfs2-disk3:/data/rustfs3
|
||||||
|
networks: [rustfs-issue-2815-net]
|
||||||
|
ports:
|
||||||
|
- "9102:9000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "sh", "-c", "curl -fsS http://127.0.0.1:9000/health || exit 1"]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 8
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
rustfs3:
|
||||||
|
image: rustfs-issue-2815-local
|
||||||
|
platform: ${RUSTFS_DOCKER_PLATFORM:-linux/arm64}
|
||||||
|
hostname: rustfs3
|
||||||
|
container_name: rustfs-issue-2815-rustfs3
|
||||||
|
environment:
|
||||||
|
RUSTFS_ADDRESS: "0.0.0.0:9000"
|
||||||
|
RUSTFS_ACCESS_KEY: "rustfsadmin"
|
||||||
|
RUSTFS_SECRET_KEY: "issue-2815-secret"
|
||||||
|
RUSTFS_RPC_SECRET: "issue-2815-rpc-secret"
|
||||||
|
RUSTFS_CONSOLE_ENABLE: "false"
|
||||||
|
RUST_LOG: "info"
|
||||||
|
RUSTFS_UNSAFE_BYPASS_DISK_CHECK: "true"
|
||||||
|
RUSTFS_VOLUMES: "http://rustfs{1...4}:9000/data/rustfs{0...3}"
|
||||||
|
volumes:
|
||||||
|
- ./data/rustfs3-disk0:/data/rustfs0
|
||||||
|
- ./data/rustfs3-disk1:/data/rustfs1
|
||||||
|
- ./data/rustfs3-disk2:/data/rustfs2
|
||||||
|
- ./data/rustfs3-disk3:/data/rustfs3
|
||||||
|
networks: [rustfs-issue-2815-net]
|
||||||
|
ports:
|
||||||
|
- "9103:9000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "sh", "-c", "curl -fsS http://127.0.0.1:9000/health || exit 1"]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 8
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
rustfs4:
|
||||||
|
image: rustfs-issue-2815-local
|
||||||
|
platform: ${RUSTFS_DOCKER_PLATFORM:-linux/arm64}
|
||||||
|
hostname: rustfs4
|
||||||
|
container_name: rustfs-issue-2815-rustfs4
|
||||||
|
environment:
|
||||||
|
RUSTFS_ADDRESS: "0.0.0.0:9000"
|
||||||
|
RUSTFS_ACCESS_KEY: "rustfsadmin"
|
||||||
|
RUSTFS_SECRET_KEY: "issue-2815-secret"
|
||||||
|
RUSTFS_RPC_SECRET: "issue-2815-rpc-secret"
|
||||||
|
RUSTFS_CONSOLE_ENABLE: "false"
|
||||||
|
RUST_LOG: "info"
|
||||||
|
RUSTFS_UNSAFE_BYPASS_DISK_CHECK: "true"
|
||||||
|
RUSTFS_VOLUMES: "http://rustfs{1...4}:9000/data/rustfs{0...3}"
|
||||||
|
volumes:
|
||||||
|
- ./data/rustfs4-disk0:/data/rustfs0
|
||||||
|
- ./data/rustfs4-disk1:/data/rustfs1
|
||||||
|
- ./data/rustfs4-disk2:/data/rustfs2
|
||||||
|
- ./data/rustfs4-disk3:/data/rustfs3
|
||||||
|
networks: [rustfs-issue-2815-net]
|
||||||
|
ports:
|
||||||
|
- "9104:9000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "sh", "-c", "curl -fsS http://127.0.0.1:9000/health || exit 1"]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 8
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
rustfs-issue-2815-net:
|
||||||
|
name: rustfs-issue-2815-net
|
||||||
@@ -27,9 +27,8 @@ updates:
|
|||||||
timezone: "Asia/Shanghai"
|
timezone: "Asia/Shanghai"
|
||||||
time: "08:00"
|
time: "08:00"
|
||||||
assignees:
|
assignees:
|
||||||
- "heihutu"
|
|
||||||
reviewers:
|
|
||||||
- "houseme"
|
- "houseme"
|
||||||
|
reviewers:
|
||||||
- "overtrue"
|
- "overtrue"
|
||||||
- "majinghe"
|
- "majinghe"
|
||||||
ignore:
|
ignore:
|
||||||
@@ -39,6 +38,8 @@ updates:
|
|||||||
versions: [ "0.23.x" ]
|
versions: [ "0.23.x" ]
|
||||||
- dependency-name: "ratelimit"
|
- dependency-name: "ratelimit"
|
||||||
versions: [ "1.x" ]
|
versions: [ "1.x" ]
|
||||||
|
- dependency-name: "ratelimit"
|
||||||
|
versions: [ "2.x" ]
|
||||||
groups:
|
groups:
|
||||||
s3s:
|
s3s:
|
||||||
update-types:
|
update-types:
|
||||||
|
|||||||
@@ -2,35 +2,34 @@
|
|||||||
Pull Request Template for RustFS
|
Pull Request Template for RustFS
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## Type of Change
|
|
||||||
- [ ] New Feature
|
|
||||||
- [ ] Bug Fix
|
|
||||||
- [ ] Documentation
|
|
||||||
- [ ] Performance Improvement
|
|
||||||
- [ ] Test/CI
|
|
||||||
- [ ] Refactor
|
|
||||||
- [ ] Other:
|
|
||||||
|
|
||||||
## Related Issues
|
## Related Issues
|
||||||
<!-- List related Issue numbers, e.g. #123 -->
|
<!--
|
||||||
|
List related issues, e.g. Fixes #123.
|
||||||
|
Use N/A when there is no related issue.
|
||||||
|
-->
|
||||||
|
|
||||||
## Summary of Changes
|
## Summary of Changes
|
||||||
<!-- Briefly describe the main changes and motivation for this PR -->
|
<!--
|
||||||
|
Briefly explain what changed and why reviewers should accept it.
|
||||||
|
Focus on behavior, compatibility, and review-relevant context.
|
||||||
|
-->
|
||||||
|
|
||||||
## Checklist
|
## Verification
|
||||||
- [ ] I have read and followed the [CONTRIBUTING.md](CONTRIBUTING.md) guidelines
|
<!--
|
||||||
- [ ] Passed `make pre-commit`
|
List the commands or checks you ran, for example:
|
||||||
- [ ] Added/updated necessary tests
|
- `make pre-commit`
|
||||||
- [ ] Documentation updated (if needed)
|
|
||||||
- [ ] CI/CD passed (if applicable)
|
Use N/A only when verification is not applicable.
|
||||||
|
-->
|
||||||
|
|
||||||
## Impact
|
## Impact
|
||||||
- [ ] Breaking change (compatibility)
|
<!--
|
||||||
- [ ] Requires doc/config/deployment update
|
Describe user-facing, compatibility, API, deployment, configuration, or
|
||||||
- [ ] Other impact:
|
documentation impact. Use N/A when there is no expected impact.
|
||||||
|
-->
|
||||||
|
|
||||||
## Additional Notes
|
## Additional Notes
|
||||||
<!-- Any extra information for reviewers -->
|
<!-- Any extra information for reviewers, or N/A. -->
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -163,11 +163,10 @@ jobs:
|
|||||||
if [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]] || [[ "$version" == *"rc"* ]]; then
|
if [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]] || [[ "$version" == *"rc"* ]]; then
|
||||||
build_type="prerelease"
|
build_type="prerelease"
|
||||||
is_prerelease=true
|
is_prerelease=true
|
||||||
# TODO: Temporary change - currently allows alpha versions to also create latest tags
|
# Current policy: create latest tags for stable releases and selected prereleases (alpha/beta).
|
||||||
# After the version is stable, you need to remove the following line and restore the original logic (latest is created only for stable versions)
|
if [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]]; then
|
||||||
if [[ "$version" == *"alpha"* ]]; then
|
|
||||||
create_latest=true
|
create_latest=true
|
||||||
echo "🧪 Building Docker image for prerelease: $version (temporarily allowing creation of latest tag)"
|
echo "🧪 Building Docker image for prerelease: $version (creating latest tag)"
|
||||||
else
|
else
|
||||||
echo "🧪 Building Docker image for prerelease: $version"
|
echo "🧪 Building Docker image for prerelease: $version"
|
||||||
fi
|
fi
|
||||||
@@ -216,11 +215,10 @@ jobs:
|
|||||||
v*alpha*|v*beta*|v*rc*|*alpha*|*beta*|*rc*)
|
v*alpha*|v*beta*|v*rc*|*alpha*|*beta*|*rc*)
|
||||||
build_type="prerelease"
|
build_type="prerelease"
|
||||||
is_prerelease=true
|
is_prerelease=true
|
||||||
# TODO: Temporary change - currently allows alpha versions to also create latest tags
|
# Current policy: create latest tags for stable releases and selected prereleases (alpha/beta).
|
||||||
# After the version is stable, you need to remove the if block below and restore the original logic.
|
if [[ "$version" == *"alpha"* ]] || [[ "$version" == *"beta"* ]]; then
|
||||||
if [[ "$input_version" == *"alpha"* ]]; then
|
|
||||||
create_latest=true
|
create_latest=true
|
||||||
echo "🧪 Building with prerelease version: $input_version (temporarily allowing creation of latest tag)"
|
echo "🧪 Building with prerelease version: $input_version (creating latest tag)"
|
||||||
else
|
else
|
||||||
echo "🧪 Building with prerelease version: $input_version"
|
echo "🧪 Building with prerelease version: $input_version"
|
||||||
fi
|
fi
|
||||||
@@ -351,9 +349,7 @@ jobs:
|
|||||||
|
|
||||||
# Add channel tags for prereleases and latest for stable
|
# Add channel tags for prereleases and latest for stable
|
||||||
if [[ "$CREATE_LATEST" == "true" ]]; then
|
if [[ "$CREATE_LATEST" == "true" ]]; then
|
||||||
# TODO: Temporary change - the current alpha version will also create the latest tag
|
# Create latest tags for stable releases and selected prereleases when CREATE_LATEST=true.
|
||||||
# After the version is stabilized, the logic here remains unchanged, but the upstream CREATE_LATEST setting needs to be restored.
|
|
||||||
# Stable release (and temporary alpha versions)
|
|
||||||
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:latest${VARIANT_SUFFIX},${{ env.REGISTRY_GHCR }}:latest${VARIANT_SUFFIX},${{ env.REGISTRY_QUAY }}:latest${VARIANT_SUFFIX}"
|
TAGS="$TAGS,${{ env.REGISTRY_DOCKERHUB }}:latest${VARIANT_SUFFIX},${{ env.REGISTRY_GHCR }}:latest${VARIANT_SUFFIX},${{ env.REGISTRY_QUAY }}:latest${VARIANT_SUFFIX}"
|
||||||
elif [[ "$BUILD_TYPE" == "prerelease" ]]; then
|
elif [[ "$BUILD_TYPE" == "prerelease" ]]; then
|
||||||
# Prerelease channel tags (alpha, beta, rc)
|
# Prerelease channel tags (alpha, beta, rc)
|
||||||
@@ -450,10 +446,9 @@ jobs:
|
|||||||
"prerelease")
|
"prerelease")
|
||||||
echo "🧪 Prerelease Docker image has been built with ${VERSION} tags"
|
echo "🧪 Prerelease Docker image has been built with ${VERSION} tags"
|
||||||
echo "⚠️ This is a prerelease image - use with caution"
|
echo "⚠️ This is a prerelease image - use with caution"
|
||||||
# TODO: Temporary change - alpha versions currently create the latest tag
|
# Create latest tags for stable releases and selected prereleases when CREATE_LATEST=true.
|
||||||
# After the version is stable, you need to restore the following prompt information
|
if [[ "$CREATE_LATEST" == "true" ]]; then
|
||||||
if [[ "$VERSION" == *"alpha"* ]] && [[ "$CREATE_LATEST" == "true" ]]; then
|
echo "🏷️ Latest tag has been created for prerelease: $VERSION"
|
||||||
echo "🏷️ Latest tag has been created for alpha version (temporary measures)"
|
|
||||||
else
|
else
|
||||||
echo "🚫 Latest tag NOT created for prerelease"
|
echo "🚫 Latest tag NOT created for prerelease"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -18,41 +18,78 @@ on:
|
|||||||
workflow_run:
|
workflow_run:
|
||||||
workflows: [ "Build and Release" ]
|
workflows: [ "Build and Release" ]
|
||||||
types: [ completed ]
|
types: [ completed ]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Release version to publish, e.g. 1.0.0-beta.1 or v1.0.0-beta.1"
|
||||||
|
required: true
|
||||||
|
default: "1.0.0-beta.1"
|
||||||
|
type: string
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
|
||||||
new_version: ${{ github.event.workflow_run.head_branch }}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-helm-package:
|
build-helm-package:
|
||||||
runs-on: ubicloud-standard-2
|
runs-on: ubicloud-standard-2
|
||||||
# Only run on successful builds triggered by tag pushes (version format: x.y.z or x.y.z-suffix)
|
|
||||||
if: |
|
if: |
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
github.event_name == 'workflow_dispatch' ||
|
||||||
github.event.workflow_run.event == 'push' &&
|
(
|
||||||
contains(github.event.workflow_run.head_branch, '.')
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
|
github.event.workflow_run.event == 'push' &&
|
||||||
|
contains(github.event.workflow_run.head_branch, '.')
|
||||||
|
)
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
raw_tag: ${{ steps.version.outputs.raw_tag }}
|
||||||
|
app_version: ${{ steps.version.outputs.app_version }}
|
||||||
|
chart_version: ${{ steps.version.outputs.chart_version }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout helm chart repo
|
- name: Checkout helm chart repo
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Replace chart app version
|
- name: Normalize release version
|
||||||
|
id: version
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -eux
|
||||||
set -x
|
|
||||||
old_version=$(grep "^appVersion:" helm/rustfs/Chart.yaml | awk '{print $2}')
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
sed -i "s/$old_version/$new_version/g" helm/rustfs/Chart.yaml
|
RAW="${{ github.event.inputs.version }}"
|
||||||
|
else
|
||||||
|
RAW="${{ github.event.workflow_run.head_branch }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$RAW" in
|
||||||
|
refs/tags/*)
|
||||||
|
RAW_TAG="${RAW#refs/tags/}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
RAW_TAG="$RAW"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
./scripts/helm_chart_version.sh "$RAW_TAG"
|
||||||
|
|
||||||
|
- name: Replace chart version and app version
|
||||||
|
run: |
|
||||||
|
set -eux
|
||||||
|
sed -i -E 's/^version:.*/version: "${{ steps.version.outputs.chart_version }}"/' helm/rustfs/Chart.yaml
|
||||||
|
sed -i -E 's/^appVersion:.*/appVersion: "${{ steps.version.outputs.app_version }}"/' helm/rustfs/Chart.yaml
|
||||||
|
|
||||||
- name: Set up Helm
|
- name: Set up Helm
|
||||||
uses: azure/setup-helm@v4.3.0
|
uses: azure/setup-helm@v4.3.0
|
||||||
|
|
||||||
|
- name: Test Helm Chart Templates
|
||||||
|
run: ./scripts/test_helm_templates.sh
|
||||||
|
|
||||||
- name: Package Helm Chart
|
- name: Package Helm Chart
|
||||||
run: |
|
run: |
|
||||||
|
set -eux
|
||||||
cp helm/README.md helm/rustfs/
|
cp helm/README.md helm/rustfs/
|
||||||
package_version=$(echo $new_version | awk -F '-' '{print $2}' | awk -F '.' '{print $NF}')
|
helm package ./helm/rustfs \
|
||||||
helm package ./helm/rustfs --destination helm/rustfs/ --version "0.0.$package_version"
|
--destination helm/rustfs/ \
|
||||||
|
--version "${{ steps.version.outputs.chart_version }}"
|
||||||
|
|
||||||
- name: Upload helm package as artifact
|
- name: Upload helm package as artifact
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v6
|
||||||
@@ -64,6 +101,7 @@ jobs:
|
|||||||
publish-helm-package:
|
publish-helm-package:
|
||||||
runs-on: ubicloud-standard-2
|
runs-on: ubicloud-standard-2
|
||||||
needs: [ build-helm-package ]
|
needs: [ build-helm-package ]
|
||||||
|
if: needs.build-helm-package.result == 'success'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout helm package repo
|
- name: Checkout helm package repo
|
||||||
@@ -86,9 +124,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Push helm package and index file
|
- name: Push helm package and index file
|
||||||
run: |
|
run: |
|
||||||
|
set -eux
|
||||||
git config --global user.name "${{ secrets.USERNAME }}"
|
git config --global user.name "${{ secrets.USERNAME }}"
|
||||||
git config --global user.email "${{ secrets.EMAIL_ADDRESS }}"
|
git config --global user.email "${{ secrets.EMAIL_ADDRESS }}"
|
||||||
git status .
|
|
||||||
git add .
|
git add .
|
||||||
git commit -m "Update rustfs helm package with $new_version."
|
git commit -m "Update rustfs helm package with ${{ needs.build-helm-package.outputs.app_version }}." || echo "No changes to commit"
|
||||||
git push origin main
|
git push origin main
|
||||||
|
|||||||
+7
-7
@@ -326,10 +326,10 @@ The binary (`main.rs`) boots in this order:
|
|||||||
│
|
│
|
||||||
┌───────────────┼───────────────┐
|
┌───────────────┼───────────────┐
|
||||||
│ │ │
|
│ │ │
|
||||||
┌────▼────┐ ┌────▼────┐ ┌─────▼─────┐
|
┌────▼────┐ ┌────▼────┐ ┌──────▼─────┐
|
||||||
│ server │ │ admin │ │ app │
|
│ server │ │ admin │ │ app │
|
||||||
│ (HTTP) │ │(console)│ │(use-cases) │
|
│ (HTTP) │ │(console)│ │(use-cases) │
|
||||||
└────┬────┘ └────┬────┘ └─────┬─────┘
|
└────┬────┘ └────┬────┘ └──────┬─────┘
|
||||||
│ │ │
|
│ │ │
|
||||||
└───────────────┼───────────────┘
|
└───────────────┼───────────────┘
|
||||||
│
|
│
|
||||||
@@ -341,13 +341,13 @@ The binary (`main.rs`) boots in this order:
|
|||||||
│
|
│
|
||||||
┌──────────────────┼──────────────────┐
|
┌──────────────────┼──────────────────┐
|
||||||
│ │ │
|
│ │ │
|
||||||
┌─────▼─────┐ ┌──────▼──────┐ ┌──────▼──────┐
|
┌─────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
|
||||||
│ ecstore │ │ rio │ │ io-core │
|
│ ecstore │ │ rio │ │ io-core │
|
||||||
│ (87K,core) │ │ (readers) │ │ (zero-copy) │
|
│ (87K,core) │ │ (readers) │ │ (zero-copy) │
|
||||||
└─────┬──────┘ └─────────────┘ └─────────────┘
|
└─────┬──────┘ └─────────────┘ └─────────────┘
|
||||||
│
|
│
|
||||||
┌─────┬──┼──┬─────┬──────┐
|
┌─────┬──┼──┬─────┬──────┐
|
||||||
│ │ │ │ │ │
|
│ │ │ │ │ │
|
||||||
common utils config policy filemeta ...
|
common utils config policy filemeta ...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Generated
+441
-540
File diff suppressed because it is too large
Load Diff
+59
-58
@@ -59,7 +59,7 @@ edition = "2024"
|
|||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
repository = "https://github.com/rustfs/rustfs"
|
repository = "https://github.com/rustfs/rustfs"
|
||||||
rust-version = "1.93.0"
|
rust-version = "1.93.0"
|
||||||
version = "0.0.5"
|
version = "1.0.0-beta.1"
|
||||||
homepage = "https://rustfs.com"
|
homepage = "https://rustfs.com"
|
||||||
description = "RustFS is a high-performance distributed object storage software built using Rust, one of the most popular languages worldwide. "
|
description = "RustFS is a high-performance distributed object storage software built using Rust, one of the most popular languages worldwide. "
|
||||||
keywords = ["RustFS", "Minio", "object-storage", "filesystem", "s3"]
|
keywords = ["RustFS", "Minio", "object-storage", "filesystem", "s3"]
|
||||||
@@ -76,46 +76,46 @@ redundant_clone = "warn"
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
# RustFS Internal Crates
|
# RustFS Internal Crates
|
||||||
rustfs = { path = "./rustfs", version = "0.0.5" }
|
rustfs = { path = "./rustfs", version = "1.0.0-beta.1" }
|
||||||
rustfs-heal = { path = "crates/heal", version = "0.0.5" }
|
rustfs-heal = { path = "crates/heal", version = "1.0.0-beta.1" }
|
||||||
rustfs-appauth = { path = "crates/appauth", version = "0.0.5" }
|
rustfs-appauth = { path = "crates/appauth", version = "1.0.0-beta.1" }
|
||||||
rustfs-audit = { path = "crates/audit", version = "0.0.5" }
|
rustfs-audit = { path = "crates/audit", version = "1.0.0-beta.1" }
|
||||||
rustfs-checksums = { path = "crates/checksums", version = "0.0.5" }
|
rustfs-checksums = { path = "crates/checksums", version = "1.0.0-beta.1" }
|
||||||
rustfs-common = { path = "crates/common", version = "0.0.5" }
|
rustfs-common = { path = "crates/common", version = "1.0.0-beta.1" }
|
||||||
rustfs-config = { path = "./crates/config", version = "0.0.5" }
|
rustfs-config = { path = "./crates/config", version = "1.0.0-beta.1" }
|
||||||
rustfs-concurrency = { path = "./crates/concurrency", version = "0.0.5" }
|
rustfs-concurrency = { path = "./crates/concurrency", version = "1.0.0-beta.1" }
|
||||||
rustfs-credentials = { path = "crates/credentials", version = "0.0.5" }
|
rustfs-credentials = { path = "crates/credentials", version = "1.0.0-beta.1" }
|
||||||
rustfs-crypto = { path = "crates/crypto", version = "0.0.5" }
|
rustfs-crypto = { path = "crates/crypto", version = "1.0.0-beta.1" }
|
||||||
rustfs-ecstore = { path = "crates/ecstore", version = "0.0.5" }
|
rustfs-ecstore = { path = "crates/ecstore", version = "1.0.0-beta.1" }
|
||||||
rustfs-filemeta = { path = "crates/filemeta", version = "0.0.5" }
|
rustfs-filemeta = { path = "crates/filemeta", version = "1.0.0-beta.1" }
|
||||||
rustfs-iam = { path = "crates/iam", version = "0.0.5" }
|
rustfs-iam = { path = "crates/iam", version = "1.0.0-beta.1" }
|
||||||
rustfs-keystone = { path = "crates/keystone", version = "0.0.5" }
|
rustfs-keystone = { path = "crates/keystone", version = "1.0.0-beta.1" }
|
||||||
rustfs-kms = { path = "crates/kms", version = "0.0.5" }
|
rustfs-kms = { path = "crates/kms", version = "1.0.0-beta.1" }
|
||||||
rustfs-lock = { path = "crates/lock", version = "0.0.5" }
|
rustfs-lock = { path = "crates/lock", version = "1.0.0-beta.1" }
|
||||||
rustfs-madmin = { path = "crates/madmin", version = "0.0.5" }
|
rustfs-madmin = { path = "crates/madmin", version = "1.0.0-beta.1" }
|
||||||
rustfs-notify = { path = "crates/notify", version = "0.0.5" }
|
rustfs-notify = { path = "crates/notify", version = "1.0.0-beta.1" }
|
||||||
rustfs-io-metrics = { path = "crates/io-metrics", version = "0.0.5" }
|
rustfs-io-metrics = { path = "crates/io-metrics", version = "1.0.0-beta.1" }
|
||||||
rustfs-io-core = { path = "crates/io-core", version = "0.0.5" }
|
rustfs-io-core = { path = "crates/io-core", version = "1.0.0-beta.1" }
|
||||||
rustfs-object-capacity = { path = "crates/object-capacity", version = "0.0.5" }
|
rustfs-object-capacity = { path = "crates/object-capacity", version = "1.0.0-beta.1" }
|
||||||
rustfs-obs = { path = "crates/obs", version = "0.0.5" }
|
rustfs-obs = { path = "crates/obs", version = "1.0.0-beta.1" }
|
||||||
rustfs-policy = { path = "crates/policy", version = "0.0.5" }
|
rustfs-policy = { path = "crates/policy", version = "1.0.0-beta.1" }
|
||||||
rustfs-protos = { path = "crates/protos", version = "0.0.5" }
|
rustfs-protos = { path = "crates/protos", version = "1.0.0-beta.1" }
|
||||||
rustfs-protocols = { path = "crates/protocols", version = "0.0.5" }
|
rustfs-protocols = { path = "crates/protocols", version = "1.0.0-beta.1" }
|
||||||
rustfs-rio = { path = "crates/rio", version = "0.0.5" }
|
rustfs-rio = { path = "crates/rio", version = "1.0.0-beta.1" }
|
||||||
rustfs-s3-common = { path = "crates/s3-common", version = "0.0.5" }
|
rustfs-s3-common = { path = "crates/s3-common", version = "1.0.0-beta.1" }
|
||||||
rustfs-s3select-api = { path = "crates/s3select-api", version = "0.0.5" }
|
rustfs-s3select-api = { path = "crates/s3select-api", version = "1.0.0-beta.1" }
|
||||||
rustfs-s3select-query = { path = "crates/s3select-query", version = "0.0.5" }
|
rustfs-s3select-query = { path = "crates/s3select-query", version = "1.0.0-beta.1" }
|
||||||
rustfs-scanner = { path = "crates/scanner", version = "0.0.5" }
|
rustfs-scanner = { path = "crates/scanner", version = "1.0.0-beta.1" }
|
||||||
rustfs-signer = { path = "crates/signer", version = "0.0.5" }
|
rustfs-signer = { path = "crates/signer", version = "1.0.0-beta.1" }
|
||||||
rustfs-trusted-proxies = { path = "crates/trusted-proxies", version = "0.0.5" }
|
rustfs-trusted-proxies = { path = "crates/trusted-proxies", version = "1.0.0-beta.1" }
|
||||||
rustfs-targets = { path = "crates/targets", version = "0.0.5" }
|
rustfs-targets = { path = "crates/targets", version = "1.0.0-beta.1" }
|
||||||
rustfs-utils = { path = "crates/utils", version = "0.0.5" }
|
rustfs-utils = { path = "crates/utils", version = "1.0.0-beta.1" }
|
||||||
rustfs-workers = { path = "crates/workers", version = "0.0.5" }
|
rustfs-workers = { path = "crates/workers", version = "1.0.0-beta.1" }
|
||||||
rustfs-zip = { path = "./crates/zip", version = "0.0.5" }
|
rustfs-zip = { path = "./crates/zip", version = "1.0.0-beta.1" }
|
||||||
|
|
||||||
# Async Runtime and Networking
|
# Async Runtime and Networking
|
||||||
async-channel = "2.5.0"
|
async-channel = "2.5.0"
|
||||||
async-compression = { version = "0.4.41" }
|
async-compression = { version = "0.4.42" }
|
||||||
async-recursion = "1.1.1"
|
async-recursion = "1.1.1"
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
async-nats = "0.47.0"
|
async-nats = "0.47.0"
|
||||||
@@ -131,9 +131,10 @@ hyper-util = { version = "0.1.20", features = ["tokio", "server-auto", "server-g
|
|||||||
http = "1.4.0"
|
http = "1.4.0"
|
||||||
http-body = "1.0.1"
|
http-body = "1.0.1"
|
||||||
http-body-util = "0.1.3"
|
http-body-util = "0.1.3"
|
||||||
reqwest = { version = "0.13.2", default-features = false, features = ["rustls", "charset", "http2", "system-proxy", "stream", "json", "blocking", "query", "form"] }
|
reqwest = { version = "0.13.3", default-features = false, features = ["rustls", "charset", "http2", "system-proxy", "stream", "json", "blocking", "query", "form"] }
|
||||||
|
rustfs-kafka-async = { version = "1.2.0" }
|
||||||
socket2 = { version = "0.6.3", features = ["all"] }
|
socket2 = { version = "0.6.3", features = ["all"] }
|
||||||
tokio = { version = "1.52.1", features = ["fs", "rt-multi-thread"] }
|
tokio = { version = "1.52.2", features = ["fs", "rt-multi-thread"] }
|
||||||
tokio-rustls = { version = "0.26.4", default-features = false, features = ["logging", "tls12", "aws-lc-rs"] }
|
tokio-rustls = { version = "0.26.4", default-features = false, features = ["logging", "tls12", "aws-lc-rs"] }
|
||||||
tokio-stream = { version = "0.1.18" }
|
tokio-stream = { version = "0.1.18" }
|
||||||
tokio-test = "0.4.5"
|
tokio-test = "0.4.5"
|
||||||
@@ -151,7 +152,7 @@ byteorder = "1.5.0"
|
|||||||
flatbuffers = "25.12.19"
|
flatbuffers = "25.12.19"
|
||||||
form_urlencoded = "1.2.2"
|
form_urlencoded = "1.2.2"
|
||||||
prost = "0.14.3"
|
prost = "0.14.3"
|
||||||
quick-xml = "0.39.2"
|
quick-xml = "0.39.3"
|
||||||
rmp = { version = "0.8.15" }
|
rmp = { version = "0.8.15" }
|
||||||
rmp-serde = { version = "1.3.1" }
|
rmp-serde = { version = "1.3.1" }
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
@@ -167,10 +168,10 @@ crc-fast = "1.9.0"
|
|||||||
hmac = { version = "0.13.0" }
|
hmac = { version = "0.13.0" }
|
||||||
jsonwebtoken = { version = "10.3.0", features = ["aws_lc_rs"] }
|
jsonwebtoken = { version = "10.3.0", features = ["aws_lc_rs"] }
|
||||||
openidconnect = { version = "4.0", default-features = false }
|
openidconnect = { version = "4.0", default-features = false }
|
||||||
pbkdf2 = "0.13.0-rc.10"
|
pbkdf2 = "0.13.0"
|
||||||
rsa = { version = "0.10.0-rc.17" }
|
rsa = { version = "0.10.0-rc.18" }
|
||||||
rustls = { version = "0.23.38", default-features = false, features = ["aws-lc-rs", "logging", "tls12", "prefer-post-quantum", "std"] }
|
rustls = { version = "0.23.40", default-features = false, features = ["aws-lc-rs", "logging", "tls12", "prefer-post-quantum", "std"] }
|
||||||
rustls-pki-types = "1.14.0"
|
rustls-pki-types = "1.14.1"
|
||||||
sha1 = "0.11.0"
|
sha1 = "0.11.0"
|
||||||
sha2 = "0.11.0"
|
sha2 = "0.11.0"
|
||||||
subtle = "2.6"
|
subtle = "2.6"
|
||||||
@@ -179,18 +180,18 @@ zeroize = { version = "1.8.2", features = ["derive"] }
|
|||||||
# Time and Date
|
# Time and Date
|
||||||
chrono = { version = "0.4.44", features = ["serde"] }
|
chrono = { version = "0.4.44", features = ["serde"] }
|
||||||
humantime = "2.3.0"
|
humantime = "2.3.0"
|
||||||
jiff = { version = "0.2.23", features = ["serde"] }
|
jiff = { version = "0.2.24", features = ["serde"] }
|
||||||
time = { version = "0.3.47", features = ["std", "parsing", "formatting", "macros", "serde"] }
|
time = { version = "0.3.47", features = ["std", "parsing", "formatting", "macros", "serde"] }
|
||||||
|
|
||||||
# Utilities and Tools
|
# Utilities and Tools
|
||||||
anyhow = "1.0.102"
|
anyhow = "1.0.102"
|
||||||
arc-swap = "1.9.1"
|
arc-swap = "1.9.1"
|
||||||
astral-tokio-tar = "0.6.0"
|
astral-tokio-tar = "0.6.1"
|
||||||
atoi = "2.0.0"
|
atoi = "2.0.0"
|
||||||
atomic_enum = "0.3.0"
|
atomic_enum = "0.3.0"
|
||||||
aws-config = { version = "1.8.15" }
|
aws-config = { version = "1.8.16" }
|
||||||
aws-credential-types = { version = "1.2.14" }
|
aws-credential-types = { version = "1.2.14" }
|
||||||
aws-sdk-s3 = { version = "1.129.0", default-features = false, features = ["sigv4a", "default-https-client", "rt-tokio"] }
|
aws-sdk-s3 = { version = "1.131.0", default-features = false, features = ["sigv4a", "default-https-client", "rt-tokio"] }
|
||||||
aws-smithy-http-client = { version = "1.1.12", default-features = false, features = ["default-client", "rustls-aws-lc"] }
|
aws-smithy-http-client = { version = "1.1.12", default-features = false, features = ["default-client", "rustls-aws-lc"] }
|
||||||
aws-smithy-types = { version = "1.4.7" }
|
aws-smithy-types = { version = "1.4.7" }
|
||||||
backtrace = "0.3.76"
|
backtrace = "0.3.76"
|
||||||
@@ -220,9 +221,9 @@ hex-simd = "0.8.0"
|
|||||||
highway = { version = "1.3.0" }
|
highway = { version = "1.3.0" }
|
||||||
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
ipnetwork = { version = "0.21.1", features = ["serde"] }
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
libc = "0.2.185"
|
libc = "0.2.186"
|
||||||
libsystemd = "0.7.2"
|
libsystemd = "0.7.2"
|
||||||
local-ip-address = "0.6.11"
|
local-ip-address = "0.6.12"
|
||||||
memmap2 = "0.9.10"
|
memmap2 = "0.9.10"
|
||||||
lz4 = "1.28.1"
|
lz4 = "1.28.1"
|
||||||
matchit = "0.9.2"
|
matchit = "0.9.2"
|
||||||
@@ -246,14 +247,14 @@ rayon = "1.12.0"
|
|||||||
reed-solomon-erasure = { version = "6.0", default-features = false, features = ["std", "simd-accel"] }
|
reed-solomon-erasure = { version = "6.0", default-features = false, features = ["std", "simd-accel"] }
|
||||||
reed-solomon-simd = "3.1.0"
|
reed-solomon-simd = "3.1.0"
|
||||||
regex = { version = "1.12.3" }
|
regex = { version = "1.12.3" }
|
||||||
rumqttc = { package = "rumqttc-next", version = "0.30.0", features = ["websocket"] }
|
rumqttc = { package = "rumqttc-next", version = "0.30.1", features = ["websocket"] }
|
||||||
rustix = { version = "1.1.4", features = ["fs"] }
|
rustix = { version = "1.1.4", features = ["fs"] }
|
||||||
rust-embed = { version = "8.11.0" }
|
rust-embed = { version = "8.11.0" }
|
||||||
rustc-hash = { version = "2.1.2" }
|
rustc-hash = { version = "2.1.2" }
|
||||||
s3s = { git = "https://github.com/rustfs/s3s", rev = "a3b16608df35aaeed8fff08b4988d03f4ca9445b", features = ["minio"] }
|
s3s = { git = "https://github.com/rustfs/s3s", rev = "a3b16608df35aaeed8fff08b4988d03f4ca9445b", features = ["minio"] }
|
||||||
serial_test = "3.4.0"
|
serial_test = "3.4.0"
|
||||||
shadow-rs = { version = "1.7.1", default-features = false }
|
shadow-rs = { version = "2.0.0", default-features = false }
|
||||||
siphasher = "1.0.2"
|
siphasher = "1.0.3"
|
||||||
smallvec = { version = "1.15.1", features = ["serde"] }
|
smallvec = { version = "1.15.1", features = ["serde"] }
|
||||||
smartstring = "1.0.1"
|
smartstring = "1.0.1"
|
||||||
snafu = "0.9.0"
|
snafu = "0.9.0"
|
||||||
@@ -279,11 +280,11 @@ walkdir = "2.5.0"
|
|||||||
wildmatch = { version = "2.6.1", features = ["serde"] }
|
wildmatch = { version = "2.6.1", features = ["serde"] }
|
||||||
windows = { version = "0.62.2" }
|
windows = { version = "0.62.2" }
|
||||||
xxhash-rust = { version = "0.8.15", features = ["xxh64", "xxh3"] }
|
xxhash-rust = { version = "0.8.15", features = ["xxh64", "xxh3"] }
|
||||||
zip = "8.5.1"
|
zip = "8.6.0"
|
||||||
zstd = "0.13.3"
|
zstd = "0.13.3"
|
||||||
|
|
||||||
# Observability and Metrics
|
# Observability and Metrics
|
||||||
metrics = "0.24.3"
|
metrics = "0.24.5"
|
||||||
dial9-tokio-telemetry = "0.3"
|
dial9-tokio-telemetry = "0.3"
|
||||||
opentelemetry = { version = "0.31.0" }
|
opentelemetry = { version = "0.31.0" }
|
||||||
opentelemetry-appender-tracing = { version = "0.31.1", features = ["experimental_use_tracing_span_context", "experimental_metadata_attributes", "spec_unstable_logs_enabled"] }
|
opentelemetry-appender-tracing = { version = "0.31.1", features = ["experimental_use_tracing_span_context", "experimental_metadata_attributes", "spec_unstable_logs_enabled"] }
|
||||||
@@ -291,12 +292,12 @@ opentelemetry-otlp = { version = "0.31.1", features = ["gzip-http", "reqwest-rus
|
|||||||
opentelemetry_sdk = { version = "0.31.0" }
|
opentelemetry_sdk = { version = "0.31.0" }
|
||||||
opentelemetry-semantic-conventions = { version = "0.31.0", features = ["semconv_experimental"] }
|
opentelemetry-semantic-conventions = { version = "0.31.0", features = ["semconv_experimental"] }
|
||||||
opentelemetry-stdout = { version = "0.31.0" }
|
opentelemetry-stdout = { version = "0.31.0" }
|
||||||
pyroscope = { version = "2.0.0", features = ["backend-pprof-rs"] }
|
pyroscope = { version = "2.0.3", features = ["backend-pprof-rs"] }
|
||||||
|
|
||||||
# FTP and SFTP
|
# FTP and SFTP
|
||||||
libunftp = { version = "0.23.0", features = ["experimental"] }
|
libunftp = { version = "0.23.0", features = ["experimental"] }
|
||||||
unftp-core = "0.1.0"
|
unftp-core = "0.1.0"
|
||||||
suppaftp = { version = "8.0.2", features = ["tokio", "tokio-rustls-aws-lc-rs"] }
|
suppaftp = { version = "8.0.3", features = ["tokio", "tokio-rustls-aws-lc-rs"] }
|
||||||
rcgen = "0.14.7"
|
rcgen = "0.14.7"
|
||||||
|
|
||||||
# WebDAV
|
# WebDAV
|
||||||
|
|||||||
+13
-2
@@ -29,8 +29,20 @@ RUN set -eux; \
|
|||||||
if [ "$RELEASE" = "latest" ]; then \
|
if [ "$RELEASE" = "latest" ]; then \
|
||||||
TAG="$(curl -fsSL https://api.github.com/repos/rustfs/rustfs/releases \
|
TAG="$(curl -fsSL https://api.github.com/repos/rustfs/rustfs/releases \
|
||||||
| grep -o '"tag_name": "[^"]*"' | cut -d'"' -f4 | head -n 1)"; \
|
| grep -o '"tag_name": "[^"]*"' | cut -d'"' -f4 | head -n 1)"; \
|
||||||
|
RELEASE_JSON="$(curl -fsSL "https://api.github.com/repos/rustfs/rustfs/releases/tags/$TAG")"; \
|
||||||
else \
|
else \
|
||||||
TAG="$RELEASE"; \
|
TAG="$RELEASE"; \
|
||||||
|
RELEASE_JSON="$(curl -fsSL "https://api.github.com/repos/rustfs/rustfs/releases/tags/$TAG" 2>/dev/null || true)"; \
|
||||||
|
if [ -z "$RELEASE_JSON" ]; then \
|
||||||
|
if [ "${TAG#v}" = "$TAG" ]; then \
|
||||||
|
ALT_TAG="v$TAG"; \
|
||||||
|
else \
|
||||||
|
ALT_TAG="${TAG#v}"; \
|
||||||
|
fi; \
|
||||||
|
echo "Primary tag lookup failed, retrying with alternate tag: $ALT_TAG"; \
|
||||||
|
RELEASE_JSON="$(curl -fsSL "https://api.github.com/repos/rustfs/rustfs/releases/tags/$ALT_TAG")"; \
|
||||||
|
TAG="$ALT_TAG"; \
|
||||||
|
fi; \
|
||||||
fi; \
|
fi; \
|
||||||
echo "Using tag: $TAG (arch pattern: $ARCH_SUBSTR)"; \
|
echo "Using tag: $TAG (arch pattern: $ARCH_SUBSTR)"; \
|
||||||
# Find download URL in assets list for this tag that contains arch substring and ends with .zip
|
# Find download URL in assets list for this tag that contains arch substring and ends with .zip
|
||||||
@@ -87,8 +99,7 @@ RUN addgroup -g 10001 -S rustfs && \
|
|||||||
chown -R rustfs:rustfs /data /logs && \
|
chown -R rustfs:rustfs /data /logs && \
|
||||||
chmod 0750 /data /logs
|
chmod 0750 /data /logs
|
||||||
|
|
||||||
ENV RUSTFS_CORS_ALLOWED_ORIGINS="*" \
|
ENV RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="*" \
|
||||||
RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="*" \
|
|
||||||
RUSTFS_VOLUMES="/data" \
|
RUSTFS_VOLUMES="/data" \
|
||||||
RUSTFS_OBS_LOGGER_LEVEL=warn \
|
RUSTFS_OBS_LOGGER_LEVEL=warn \
|
||||||
RUSTFS_OBS_LOG_DIRECTORY=/logs \
|
RUSTFS_OBS_LOG_DIRECTORY=/logs \
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
FROM rust:1.91-trixie
|
FROM rust:1.93-trixie
|
||||||
|
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
export DEBIAN_FRONTEND=noninteractive; \
|
export DEBIAN_FRONTEND=noninteractive; \
|
||||||
|
|||||||
+13
-3
@@ -31,14 +31,25 @@ RUN set -eux; \
|
|||||||
arm64) ARCH_SUBSTR="aarch64-gnu" ;; \
|
arm64) ARCH_SUBSTR="aarch64-gnu" ;; \
|
||||||
*) echo "Unsupported TARGETARCH=$TARGETARCH" >&2; exit 1 ;; \
|
*) echo "Unsupported TARGETARCH=$TARGETARCH" >&2; exit 1 ;; \
|
||||||
esac; \
|
esac; \
|
||||||
\
|
|
||||||
if [ "$RELEASE" = "latest" ]; then \
|
if [ "$RELEASE" = "latest" ]; then \
|
||||||
TAG="$(curl -fsSL https://api.github.com/repos/rustfs/rustfs/releases \
|
TAG="$(curl -fsSL https://api.github.com/repos/rustfs/rustfs/releases \
|
||||||
| grep -o '"tag_name": "[^"]*"' | cut -d'"' -f4 | head -n 1)"; \
|
| grep -o '"tag_name": "[^"]*"' | cut -d'"' -f4 | head -n 1)"; \
|
||||||
|
RELEASE_JSON="$(curl -fsSL "https://api.github.com/repos/rustfs/rustfs/releases/tags/$TAG")"; \
|
||||||
else \
|
else \
|
||||||
TAG="$RELEASE"; \
|
TAG="$RELEASE"; \
|
||||||
|
RELEASE_JSON="$(curl -fsSL "https://api.github.com/repos/rustfs/rustfs/releases/tags/$TAG" 2>/dev/null || true)"; \
|
||||||
|
if [ -z "$RELEASE_JSON" ]; then \
|
||||||
|
if [ "${TAG#v}" = "$TAG" ]; then \
|
||||||
|
ALT_TAG="v$TAG"; \
|
||||||
|
else \
|
||||||
|
ALT_TAG="${TAG#v}"; \
|
||||||
|
fi; \
|
||||||
|
echo "Primary tag lookup failed, retrying with alternate tag: $ALT_TAG"; \
|
||||||
|
RELEASE_JSON="$(curl -fsSL "https://api.github.com/repos/rustfs/rustfs/releases/tags/$ALT_TAG")"; \
|
||||||
|
TAG="$ALT_TAG"; \
|
||||||
|
fi; \
|
||||||
fi; \
|
fi; \
|
||||||
\
|
echo "Using tag: $TAG (arch pattern: $ARCH_SUBSTR)"; \
|
||||||
URL="$(curl -fsSL "https://api.github.com/repos/rustfs/rustfs/releases/tags/$TAG" \
|
URL="$(curl -fsSL "https://api.github.com/repos/rustfs/rustfs/releases/tags/$TAG" \
|
||||||
| grep -o "\"browser_download_url\": \"[^\"]*${ARCH_SUBSTR}[^\"]*\\.zip\"" \
|
| grep -o "\"browser_download_url\": \"[^\"]*${ARCH_SUBSTR}[^\"]*\\.zip\"" \
|
||||||
| cut -d'"' -f4 | head -n 1)"; \
|
| cut -d'"' -f4 | head -n 1)"; \
|
||||||
@@ -97,7 +108,6 @@ ENV RUSTFS_ADDRESS=":9000" \
|
|||||||
RUSTFS_ACCESS_KEY="rustfsadmin" \
|
RUSTFS_ACCESS_KEY="rustfsadmin" \
|
||||||
RUSTFS_SECRET_KEY="rustfsadmin" \
|
RUSTFS_SECRET_KEY="rustfsadmin" \
|
||||||
RUSTFS_CONSOLE_ENABLE="true" \
|
RUSTFS_CONSOLE_ENABLE="true" \
|
||||||
RUSTFS_CORS_ALLOWED_ORIGINS="*" \
|
|
||||||
RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="*" \
|
RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="*" \
|
||||||
RUSTFS_VOLUMES="/data" \
|
RUSTFS_VOLUMES="/data" \
|
||||||
RUSTFS_OBS_LOGGER_LEVEL=warn \
|
RUSTFS_OBS_LOGGER_LEVEL=warn \
|
||||||
|
|||||||
+30
-9
@@ -26,15 +26,17 @@
|
|||||||
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
ARG BUILDPLATFORM
|
ARG BUILDPLATFORM
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# Build stage
|
# Build stage
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
FROM rust:1.91-trixie AS builder
|
FROM rust:1.93-trixie AS builder
|
||||||
|
|
||||||
# Re-declare args after FROM
|
# Re-declare args after FROM
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
ARG BUILDPLATFORM
|
ARG BUILDPLATFORM
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
# Debug: print platforms
|
# Debug: print platforms
|
||||||
RUN echo "Build info -> BUILDPLATFORM=${BUILDPLATFORM}, TARGETPLATFORM=${TARGETPLATFORM}"
|
RUN echo "Build info -> BUILDPLATFORM=${BUILDPLATFORM}, TARGETPLATFORM=${TARGETPLATFORM}"
|
||||||
@@ -60,7 +62,15 @@ RUN set -eux; \
|
|||||||
|
|
||||||
# Optional: cross toolchain for aarch64 (only when targeting linux/arm64)
|
# Optional: cross toolchain for aarch64 (only when targeting linux/arm64)
|
||||||
RUN set -eux; \
|
RUN set -eux; \
|
||||||
if [ "${TARGETPLATFORM:-linux/amd64}" = "linux/arm64" ]; then \
|
target_platform="${TARGETPLATFORM:-}"; \
|
||||||
|
if [ -z "${target_platform}" ]; then \
|
||||||
|
case "$(uname -m)" in \
|
||||||
|
x86_64) target_platform="linux/amd64" ;; \
|
||||||
|
aarch64|arm64) target_platform="linux/arm64" ;; \
|
||||||
|
*) target_platform="linux/amd64" ;; \
|
||||||
|
esac; \
|
||||||
|
fi; \
|
||||||
|
if [ "${target_platform}" = "linux/arm64" ]; then \
|
||||||
export DEBIAN_FRONTEND=noninteractive; \
|
export DEBIAN_FRONTEND=noninteractive; \
|
||||||
apt-get update; \
|
apt-get update; \
|
||||||
apt-get install -y --no-install-recommends gcc-aarch64-linux-gnu; \
|
apt-get install -y --no-install-recommends gcc-aarch64-linux-gnu; \
|
||||||
@@ -79,6 +89,7 @@ ENV CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++
|
|||||||
ENV CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc
|
ENV CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc
|
||||||
ENV CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc
|
ENV CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc
|
||||||
ENV CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++
|
ENV CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++
|
||||||
|
ENV CARGO_TARGET_DIR=/usr/src/rustfs/target/docker-build
|
||||||
|
|
||||||
WORKDIR /usr/src/rustfs
|
WORKDIR /usr/src/rustfs
|
||||||
|
|
||||||
@@ -112,27 +123,36 @@ ENV CARGO_NET_GIT_FETCH_WITH_CLI=true \
|
|||||||
# Generate protobuf/flatbuffers code (uses protoc/flatc from distro)
|
# Generate protobuf/flatbuffers code (uses protoc/flatc from distro)
|
||||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||||
--mount=type=cache,target=/usr/local/cargo/git \
|
--mount=type=cache,target=/usr/local/cargo/git \
|
||||||
--mount=type=cache,target=/usr/src/rustfs/target \
|
--mount=type=cache,target=/usr/src/rustfs/target/docker-build \
|
||||||
cargo run --bin gproto
|
cargo run --bin gproto
|
||||||
|
|
||||||
# Build RustFS (target depends on TARGETPLATFORM)
|
# Build RustFS (target depends on TARGETPLATFORM)
|
||||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||||
--mount=type=cache,target=/usr/local/cargo/git \
|
--mount=type=cache,target=/usr/local/cargo/git \
|
||||||
--mount=type=cache,target=/usr/src/rustfs/target \
|
--mount=type=cache,target=/usr/src/rustfs/target/docker-build \
|
||||||
set -eux; \
|
set -eux; \
|
||||||
case "${TARGETPLATFORM:-linux/amd64}" in \
|
rustup target add x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; \
|
||||||
|
target_platform="${TARGETPLATFORM:-}"; \
|
||||||
|
if [ -z "${target_platform}" ]; then \
|
||||||
|
case "${TARGETARCH:-$(uname -m)}" in \
|
||||||
|
amd64|x86_64) target_platform="linux/amd64" ;; \
|
||||||
|
arm64|aarch64) target_platform="linux/arm64" ;; \
|
||||||
|
*) echo "Unsupported target architecture: ${TARGETARCH:-$(uname -m)}" >&2; exit 1 ;; \
|
||||||
|
esac; \
|
||||||
|
fi; \
|
||||||
|
case "${target_platform}" in \
|
||||||
linux/amd64) \
|
linux/amd64) \
|
||||||
echo "Building for x86_64-unknown-linux-gnu"; \
|
echo "Building for x86_64-unknown-linux-gnu"; \
|
||||||
cargo build --release --locked --target x86_64-unknown-linux-gnu --bin rustfs -j "$(nproc)"; \
|
cargo build --release --locked --target x86_64-unknown-linux-gnu --bin rustfs -j "$(nproc)"; \
|
||||||
install -m 0755 target/x86_64-unknown-linux-gnu/release/rustfs /usr/local/bin/rustfs \
|
install -m 0755 "${CARGO_TARGET_DIR}/x86_64-unknown-linux-gnu/release/rustfs" /usr/local/bin/rustfs \
|
||||||
;; \
|
;; \
|
||||||
linux/arm64) \
|
linux/arm64) \
|
||||||
echo "Building for aarch64-unknown-linux-gnu"; \
|
echo "Building for aarch64-unknown-linux-gnu"; \
|
||||||
cargo build --release --locked --target aarch64-unknown-linux-gnu --bin rustfs -j "$(nproc)"; \
|
cargo build --release --locked --target aarch64-unknown-linux-gnu --bin rustfs -j "$(nproc)"; \
|
||||||
install -m 0755 target/aarch64-unknown-linux-gnu/release/rustfs /usr/local/bin/rustfs \
|
install -m 0755 "${CARGO_TARGET_DIR}/aarch64-unknown-linux-gnu/release/rustfs" /usr/local/bin/rustfs \
|
||||||
;; \
|
;; \
|
||||||
*) \
|
*) \
|
||||||
echo "Unsupported TARGETPLATFORM=${TARGETPLATFORM}" >&2; exit 1 \
|
echo "Unsupported target platform=${target_platform}" >&2; exit 1 \
|
||||||
;; \
|
;; \
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -178,7 +198,7 @@ CMD ["cargo", "run", "--bin", "rustfs", "--"]
|
|||||||
# -----------------------------
|
# -----------------------------
|
||||||
# Runtime stage (Ubuntu minimal)
|
# Runtime stage (Ubuntu minimal)
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
FROM ubuntu:22.04
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
ARG BUILD_DATE
|
ARG BUILD_DATE
|
||||||
ARG VCS_REF
|
ARG VCS_REF
|
||||||
@@ -195,6 +215,7 @@ RUN set -eux; \
|
|||||||
apt-get update; \
|
apt-get update; \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
|
curl \
|
||||||
tzdata \
|
tzdata \
|
||||||
coreutils; \
|
coreutils; \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ chown -R 10001:10001 data logs
|
|||||||
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:latest
|
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:latest
|
||||||
|
|
||||||
# Using specific version
|
# Using specific version
|
||||||
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:1.0.0-alpha.76
|
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:v1.0.0-beta.1
|
||||||
```
|
```
|
||||||
|
|
||||||
If you use [podman](https://github.com/containers/podman) instead of docker, you can install the RustFS with the below command
|
If you use [podman](https://github.com/containers/podman) instead of docker, you can install the RustFS with the below command
|
||||||
@@ -136,6 +136,22 @@ Similarly, you can run the command with podman
|
|||||||
podman compose --profile observability up -d
|
podman compose --profile observability up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Webhook notification quick start (Docker):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name rustfs -p 9000:9000 \
|
||||||
|
-e RUSTFS_NOTIFY_ENABLE=true \
|
||||||
|
-e RUSTFS_NOTIFY_WEBHOOK_ENABLE_PRIMARY=on \
|
||||||
|
-e RUSTFS_NOTIFY_WEBHOOK_ENDPOINT_PRIMARY=http://<host-ip>:3020/webhook \
|
||||||
|
-e RUSTFS_NOTIFY_WEBHOOK_QUEUE_DIR_PRIMARY=/tmp/rustfs-events \
|
||||||
|
rustfs/rustfs:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- `RUSTFS_NOTIFY_ENABLE=true` enables the global notify module switch.
|
||||||
|
- For ARN `arn:rustfs:sqs::primary:webhook`, use instance-scoped env vars with `_PRIMARY`.
|
||||||
|
- If queue dir is omitted, default is `/opt/rustfs/events`; ensure it is writable by the container runtime user.
|
||||||
|
|
||||||
**NOTE**: We recommend reviewing the `docker-compose.yml` file before running. It defines several services including Grafana, Prometheus, and Jaeger, which are helpful for RustFS observability. If you wish to start Redis or Nginx containers, you can specify the corresponding profiles.
|
**NOTE**: We recommend reviewing the `docker-compose.yml` file before running. It defines several services including Grafana, Prometheus, and Jaeger, which are helpful for RustFS observability. If you wish to start Redis or Nginx containers, you can specify the corresponding profiles.
|
||||||
|
|
||||||
### 3\. Build from Source (Option 3) - Advanced Users
|
### 3\. Build from Source (Option 3) - Advanced Users
|
||||||
|
|||||||
+1
-1
@@ -113,7 +113,7 @@ RustFS 容器以非 root 用户 `rustfs` (UID `10001`) 运行。如果您使用
|
|||||||
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:latest
|
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:latest
|
||||||
|
|
||||||
# 使用指定版本运行
|
# 使用指定版本运行
|
||||||
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:1.0.0.alpha.68
|
docker run -d -p 9000:9000 -p 9001:9001 -v $(pwd)/data:/data -v $(pwd)/logs:/logs rustfs/rustfs:v1.0.0-beta.1
|
||||||
```
|
```
|
||||||
|
|
||||||
您也可以使用 Docker Compose。使用根目录下的 `docker-compose.yml` 文件:
|
您也可以使用 Docker Compose。使用根目录下的 `docker-compose.yml` 文件:
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ categories = ["web-programming", "development-tools", "authentication"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64-simd = { workspace = true }
|
base64-simd = { workspace = true }
|
||||||
rsa = { workspace = true }
|
rsa = { workspace = true, features = ["sha2"] }
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
|
|||||||
+123
-36
@@ -15,9 +15,13 @@
|
|||||||
use rsa::{
|
use rsa::{
|
||||||
Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey,
|
Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey,
|
||||||
pkcs8::{DecodePrivateKey, DecodePublicKey},
|
pkcs8::{DecodePrivateKey, DecodePublicKey},
|
||||||
|
pss::{BlindedSigningKey, Signature, VerifyingKey},
|
||||||
|
sha2::Sha256,
|
||||||
|
signature::{RandomizedSigner, Verifier},
|
||||||
|
traits::PublicKeyParts,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::io::{Error, Result};
|
use std::io::{Error, ErrorKind, Result};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||||||
pub struct Token {
|
pub struct Token {
|
||||||
@@ -25,10 +29,11 @@ pub struct Token {
|
|||||||
pub expired: u64, // Expiry time (UNIX timestamp)
|
pub expired: u64, // Expiry time (UNIX timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Public key generation Token
|
/// Legacy public-key encryption Token encoder.
|
||||||
/// [token] Token object
|
///
|
||||||
/// [key] Public key string
|
/// Use `sign_license_token` for license issuance so verifiers only need a
|
||||||
/// Returns the encrypted string processed by base64
|
/// public key.
|
||||||
|
#[deprecated(note = "use sign_license_token for signed license issuance")]
|
||||||
pub fn gencode(token: &Token, key: &str) -> Result<String> {
|
pub fn gencode(token: &Token, key: &str) -> Result<String> {
|
||||||
let data = serde_json::to_vec(token)?;
|
let data = serde_json::to_vec(token)?;
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand::rng();
|
||||||
@@ -37,35 +42,62 @@ pub fn gencode(token: &Token, key: &str) -> Result<String> {
|
|||||||
Ok(base64_simd::URL_SAFE_NO_PAD.encode_to_string(&encrypted_data))
|
Ok(base64_simd::URL_SAFE_NO_PAD.encode_to_string(&encrypted_data))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Private key resolution Token
|
/// Legacy private-key Token decoder.
|
||||||
/// [token] Encrypted string processed by base64
|
///
|
||||||
/// [key] Private key string
|
/// Use `parse_signed_license_token` or `parse_license_with_public_key` for
|
||||||
/// Return to the Token object
|
/// license verification so runtime services never need private key material.
|
||||||
|
#[deprecated(note = "use parse_signed_license_token or parse_license_with_public_key for signed license verification")]
|
||||||
pub fn parse(token: &str, key: &str) -> Result<Token> {
|
pub fn parse(token: &str, key: &str) -> Result<Token> {
|
||||||
let encrypted_data = base64_simd::URL_SAFE_NO_PAD
|
let encrypted_data = base64_simd::URL_SAFE_NO_PAD
|
||||||
.decode_to_vec(token.as_bytes())
|
.decode_to_vec(token.as_bytes())
|
||||||
.map_err(Error::other)?;
|
.map_err(Error::other)?;
|
||||||
let private_key = RsaPrivateKey::from_pkcs8_pem(key).map_err(Error::other)?;
|
let private_key = RsaPrivateKey::from_pkcs8_pem(key).map_err(Error::other)?;
|
||||||
let decrypted_data = private_key.decrypt(Pkcs1v15Encrypt, &encrypted_data).map_err(Error::other)?;
|
let decrypted_data = private_key.decrypt(Pkcs1v15Encrypt, &encrypted_data).map_err(Error::other)?;
|
||||||
let res: Token = serde_json::from_slice(&decrypted_data)?;
|
serde_json::from_slice(&decrypted_data).map_err(Error::other)
|
||||||
Ok(res)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_license(license: &str) -> Result<Token> {
|
/// Signs a license token with an RSA private key.
|
||||||
parse(license, TEST_PRIVATE_KEY)
|
///
|
||||||
// match parse(license, TEST_PRIVATE_KEY) {
|
/// The returned token is base64url(signature || payload), where the signature is
|
||||||
// Ok(token) => {
|
/// RSASSA-PSS over the JSON payload using SHA-256.
|
||||||
// if token.expired > SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() {
|
pub fn sign_license_token(token: &Token, private_key_pem: &str) -> Result<String> {
|
||||||
// Ok(token)
|
let payload = serde_json::to_vec(token)?;
|
||||||
// } else {
|
let mut rng = rand::rng();
|
||||||
// Err("Token expired".into())
|
let private_key = RsaPrivateKey::from_pkcs8_pem(private_key_pem).map_err(Error::other)?;
|
||||||
// }
|
let signing_key = BlindedSigningKey::<Sha256>::new(private_key);
|
||||||
// }
|
let signature: Signature = signing_key.try_sign_with_rng(&mut rng, &payload).map_err(Error::other)?;
|
||||||
// Err(e) => Err(e),
|
let signature: Box<[u8]> = signature.into();
|
||||||
// }
|
|
||||||
|
let mut signed_payload = Vec::with_capacity(signature.as_ref().len() + payload.len());
|
||||||
|
signed_payload.extend_from_slice(signature.as_ref());
|
||||||
|
signed_payload.extend_from_slice(&payload);
|
||||||
|
|
||||||
|
Ok(base64_simd::URL_SAFE_NO_PAD.encode_to_string(&signed_payload))
|
||||||
}
|
}
|
||||||
|
|
||||||
static TEST_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCj86SrJIuxSxR6\nBJ/dlJEUIj6NeBRnhLQlCDdovuz61+7kJXVcxaR66w4m8W7SLEUP+IlPtnn6vmiG\n7XMhGNHIr7r1JsEVVLhZmL3tKI66DEZl786ZhG81BWqUlmcooIPS8UEPZNqJXLuz\nVGhxNyVGbj/tV7QC2pSISnKaixc+nrhxvo7w56p5qrm9tik0PjTgfZsUePkoBsSN\npoRkAauS14MAzK6HGB75CzG3dZqXUNWSWVocoWtQbZUwFGXyzU01ammsHQDvc2xu\nK1RQpd1qYH5bOWZ0N0aPFwT0r59HztFXg9sbjsnuhO1A7OiUOkc6iGVuJ0wm/9nA\nwZIBqzgjAgMBAAECggEAPMpeSEbotPhNw2BrllE76ec4omPfzPJbiU+em+wPGoNu\nRJHPDnMKJbl6Kd5jZPKdOOrCnxfd6qcnQsBQa/kz7+GYxMV12l7ra+1Cnujm4v0i\nLTHZvPpp8ZLsjeOmpF3AAzsJEJgon74OqtOlVjVIUPEYKvzV9ijt4gsYq0zfdYv0\nhrTMzyrGM4/UvKLsFIBROAfCeWfA7sXLGH8JhrRAyDrtCPzGtyyAmzoHKHtHafcB\nuyPFw/IP8otAgpDk5iiQPNkH0WwzAQIm12oHuNUa66NwUK4WEjXTnDg8KeWLHHNv\nIfN8vdbZchMUpMIvvkr7is315d8f2cHCB5gEO+GWAQKBgQDR/0xNll+FYaiUKCPZ\nvkOCAd3l5mRhsqnjPQ/6Ul1lAyYWpoJSFMrGGn/WKTa/FVFJRTGbBjwP+Mx10bfb\ngUg2GILDTISUh54fp4zngvTi9w4MWGKXrb7I1jPkM3vbJfC/v2fraQ/r7qHPpO2L\nf6ZbGxasIlSvr37KeGoelwcAQQKBgQDH3hmOTS2Hl6D4EXdq5meHKrfeoicGN7m8\noQK7u8iwn1R9zK5nh6IXxBhKYNXNwdCQtBZVRvFjjZ56SZJb7lKqa1BcTsgJfZCy\nnI3Uu4UykrECAH8AVCVqBXUDJmeA2yE+gDAtYEjvhSDHpUfWxoGHr0B/Oqk2Lxc/\npRy1qV5fYwKBgBWSL/hYVf+RhIuTg/s9/BlCr9SJ0g3nGGRrRVTlWQqjRCpXeFOO\nJzYqSq9pFGKUggEQxoOyJEFPwVDo9gXqRcyov+Xn2kaXl7qQr3yoixc1YZALFDWY\nd1ySBEqQr0xXnV9U/gvEgwotPRnjSzNlLWV2ZuHPtPtG/7M0o1H5GZMBAoGAKr3N\nW0gX53o+my4pCnxRQW+aOIsWq1a5aqRIEFudFGBOUkS2Oz+fI1P1GdrRfhnnfzpz\n2DK+plp/vIkFOpGhrf4bBlJ2psjqa7fdANRFLMaAAfyXLDvScHTQTCcnVUAHQPVq\n2BlSH56pnugyj7SNuLV6pnql+wdhAmRN2m9o1h8CgYAbX2juSr4ioXwnYjOUdrIY\n4+ERvHcXdjoJmmPcAm4y5NbSqLXyU0FQmplNMt2A5LlniWVJ9KNdjAQUt60FZw/+\nr76LdxXaHNZghyx0BOs7mtq5unSQXamZ8KixasfhE9uz3ij1jXjG6hafWkS8/68I\nuWbaZqgvy7a9oPHYlKH7Jg==\n-----END PRIVATE KEY-----\n";
|
/// Verifies and parses a signed license token with an RSA public key.
|
||||||
|
pub fn parse_signed_license_token(token: &str, public_key_pem: &str) -> Result<Token> {
|
||||||
|
let signed_payload = base64_simd::URL_SAFE_NO_PAD
|
||||||
|
.decode_to_vec(token.as_bytes())
|
||||||
|
.map_err(Error::other)?;
|
||||||
|
let public_key = RsaPublicKey::from_public_key_pem(public_key_pem).map_err(Error::other)?;
|
||||||
|
let signature_len = public_key.size();
|
||||||
|
|
||||||
|
if signed_payload.len() <= signature_len {
|
||||||
|
return Err(Error::new(ErrorKind::InvalidData, "license token is missing signed payload"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (signature, payload) = signed_payload.split_at(signature_len);
|
||||||
|
let signature = Signature::try_from(signature).map_err(Error::other)?;
|
||||||
|
let verifying_key = VerifyingKey::<Sha256>::new(public_key);
|
||||||
|
verifying_key.verify(payload, &signature).map_err(Error::other)?;
|
||||||
|
|
||||||
|
serde_json::from_slice(payload).map_err(Error::other)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_license_with_public_key(license: &str, public_key: &str) -> Result<Token> {
|
||||||
|
parse_signed_license_token(license, public_key)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@@ -77,7 +109,7 @@ mod tests {
|
|||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gencode_and_parse() {
|
fn test_sign_license_token_and_parse_signed_license_token() {
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand::rng();
|
||||||
let bits = 2048;
|
let bits = 2048;
|
||||||
let private_key = RsaPrivateKey::new(&mut rng, bits).expect("Failed to generate private key");
|
let private_key = RsaPrivateKey::new(&mut rng, bits).expect("Failed to generate private key");
|
||||||
@@ -91,8 +123,31 @@ mod tests {
|
|||||||
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600, // 1 hour from now
|
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600, // 1 hour from now
|
||||||
};
|
};
|
||||||
|
|
||||||
let encoded = gencode(&token, &public_key_pem).expect("Failed to encode token");
|
let encoded = sign_license_token(&token, &private_key_pem).expect("Failed to encode token");
|
||||||
|
|
||||||
|
let decoded = parse_signed_license_token(&encoded, &public_key_pem).expect("Failed to decode token");
|
||||||
|
|
||||||
|
assert_eq!(token.name, decoded.name);
|
||||||
|
assert_eq!(token.expired, decoded.expired);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(deprecated)]
|
||||||
|
fn test_legacy_gencode_and_parse_roundtrip() {
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
let bits = 2048;
|
||||||
|
let private_key = RsaPrivateKey::new(&mut rng, bits).expect("Failed to generate private key");
|
||||||
|
let public_key = RsaPublicKey::from(&private_key);
|
||||||
|
|
||||||
|
let private_key_pem = private_key.to_pkcs8_pem(LineEnding::LF).unwrap();
|
||||||
|
let public_key_pem = public_key.to_public_key_pem(LineEnding::LF).unwrap();
|
||||||
|
|
||||||
|
let token = Token {
|
||||||
|
name: "test_app".to_string(),
|
||||||
|
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600,
|
||||||
|
};
|
||||||
|
|
||||||
|
let encoded = gencode(&token, &public_key_pem).expect("Failed to encode token");
|
||||||
let decoded = parse(&encoded, &private_key_pem).expect("Failed to decode token");
|
let decoded = parse(&encoded, &private_key_pem).expect("Failed to decode token");
|
||||||
|
|
||||||
assert_eq!(token.name, decoded.name);
|
assert_eq!(token.name, decoded.name);
|
||||||
@@ -100,28 +155,60 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_invalid_token() {
|
fn test_parse_signed_license_token_rejects_tampered_payload() {
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand::rng();
|
||||||
let private_key_pem = RsaPrivateKey::new(&mut rng, 2048)
|
let private_key = RsaPrivateKey::new(&mut rng, 2048).expect("Failed to generate private key");
|
||||||
.expect("Failed to generate private key")
|
let public_key = RsaPublicKey::from(&private_key);
|
||||||
.to_pkcs8_pem(LineEnding::LF)
|
let private_key_pem = private_key.to_pkcs8_pem(LineEnding::LF).unwrap();
|
||||||
.unwrap();
|
let public_key_pem = public_key.to_public_key_pem(LineEnding::LF).unwrap();
|
||||||
|
let token = Token {
|
||||||
|
name: "test_app".to_string(),
|
||||||
|
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600,
|
||||||
|
};
|
||||||
|
|
||||||
let invalid_token = "invalid_base64_token";
|
let encoded = sign_license_token(&token, &private_key_pem).expect("Failed to encode token");
|
||||||
let result = parse(invalid_token, &private_key_pem);
|
let mut signed_payload = base64_simd::URL_SAFE_NO_PAD
|
||||||
|
.decode_to_vec(encoded.as_bytes())
|
||||||
|
.expect("Failed to decode signed payload");
|
||||||
|
let last_byte = signed_payload.last_mut().expect("Signed payload should not be empty");
|
||||||
|
*last_byte ^= 0x01;
|
||||||
|
let tampered = base64_simd::URL_SAFE_NO_PAD.encode_to_string(&signed_payload);
|
||||||
|
|
||||||
|
let result = parse_signed_license_token(&tampered, &public_key_pem);
|
||||||
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gencode_with_invalid_key() {
|
fn test_source_does_not_embed_private_key() {
|
||||||
|
let source = include_str!("token.rs");
|
||||||
|
let forbidden = ["BEGIN", "PRIVATE KEY"].join(" ");
|
||||||
|
|
||||||
|
assert!(!source.contains(&forbidden));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_signed_license_token_rejects_invalid_token() {
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
let private_key = RsaPrivateKey::new(&mut rng, 2048).expect("Failed to generate private key");
|
||||||
|
let public_key = RsaPublicKey::from(&private_key);
|
||||||
|
let public_key_pem = public_key.to_public_key_pem(LineEnding::LF).unwrap();
|
||||||
|
|
||||||
|
let invalid_token = "invalid_base64_token";
|
||||||
|
let result = parse_signed_license_token(invalid_token, &public_key_pem);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sign_license_token_with_invalid_signing_key() {
|
||||||
let token = Token {
|
let token = Token {
|
||||||
name: "test_app".to_string(),
|
name: "test_app".to_string(),
|
||||||
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600, // 1 hour from now
|
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600, // 1 hour from now
|
||||||
};
|
};
|
||||||
|
|
||||||
let invalid_key = "invalid_public_key";
|
let invalid_key = "invalid_private_key";
|
||||||
let result = gencode(&token, invalid_key);
|
let result = sign_license_token(&token, invalid_key);
|
||||||
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,11 +41,10 @@ serde_json = { workspace = true }
|
|||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["sync", "fs", "rt-multi-thread", "rt", "time", "macros"] }
|
tokio = { workspace = true, features = ["sync", "fs", "rt-multi-thread", "rt", "time", "macros"] }
|
||||||
tracing = { workspace = true, features = ["std", "attributes"] }
|
tracing = { workspace = true, features = ["std", "attributes"] }
|
||||||
url = { workspace = true }
|
|
||||||
rumqttc = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
temp-env = { workspace = true }
|
temp-env = { workspace = true }
|
||||||
|
url = { workspace = true }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -15,13 +15,13 @@
|
|||||||
use crate::AuditEntry;
|
use crate::AuditEntry;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use rustfs_config::AUDIT_DEFAULT_DIR;
|
use rustfs_config::AUDIT_DEFAULT_DIR;
|
||||||
use rustfs_config::audit::{AUDIT_MQTT_KEYS, AUDIT_NATS_KEYS, AUDIT_PULSAR_KEYS, AUDIT_WEBHOOK_KEYS};
|
use rustfs_config::audit::{AUDIT_KAFKA_KEYS, AUDIT_MQTT_KEYS, AUDIT_NATS_KEYS, AUDIT_PULSAR_KEYS, AUDIT_WEBHOOK_KEYS};
|
||||||
use rustfs_ecstore::config::KVS;
|
use rustfs_ecstore::config::KVS;
|
||||||
use rustfs_targets::{
|
use rustfs_targets::{
|
||||||
Target,
|
Target,
|
||||||
config::{
|
config::{
|
||||||
build_mqtt_args, build_nats_args, build_pulsar_args, build_webhook_args, validate_mqtt_config, validate_nats_config,
|
build_kafka_args, build_mqtt_args, build_nats_args, build_pulsar_args, build_webhook_args, validate_kafka_config,
|
||||||
validate_pulsar_config, validate_webhook_config,
|
validate_mqtt_config, validate_nats_config, validate_pulsar_config, validate_webhook_config,
|
||||||
},
|
},
|
||||||
error::TargetError,
|
error::TargetError,
|
||||||
target::TargetType,
|
target::TargetType,
|
||||||
@@ -119,3 +119,22 @@ impl TargetFactory for PulsarTargetFactory {
|
|||||||
AUDIT_PULSAR_KEYS.iter().map(|s| s.to_string()).collect()
|
AUDIT_PULSAR_KEYS.iter().map(|s| s.to_string()).collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct KafkaTargetFactory;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TargetFactory for KafkaTargetFactory {
|
||||||
|
async fn create_target(&self, id: String, config: &KVS) -> Result<Box<dyn Target<AuditEntry> + Send + Sync>, TargetError> {
|
||||||
|
let args = build_kafka_args(config, AUDIT_DEFAULT_DIR, TargetType::AuditLog)?;
|
||||||
|
let target = rustfs_targets::target::kafka::KafkaTarget::new(id, args)?;
|
||||||
|
Ok(Box::new(target))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_config(&self, _id: &str, config: &KVS) -> Result<(), TargetError> {
|
||||||
|
validate_kafka_config(config, AUDIT_DEFAULT_DIR)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_valid_fields(&self) -> HashSet<String> {
|
||||||
|
AUDIT_KAFKA_KEYS.iter().map(|s| s.to_string()).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,9 @@
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AuditEntry, AuditError, AuditResult,
|
AuditEntry, AuditError, AuditResult,
|
||||||
factory::{MQTTTargetFactory, NATSTargetFactory, PulsarTargetFactory, TargetFactory, WebhookTargetFactory},
|
factory::{
|
||||||
|
KafkaTargetFactory, MQTTTargetFactory, NATSTargetFactory, PulsarTargetFactory, TargetFactory, WebhookTargetFactory,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use futures::stream::FuturesUnordered;
|
use futures::stream::FuturesUnordered;
|
||||||
@@ -53,6 +55,7 @@ impl AuditRegistry {
|
|||||||
registry.register(ChannelTargetType::Mqtt.as_str(), Box::new(MQTTTargetFactory));
|
registry.register(ChannelTargetType::Mqtt.as_str(), Box::new(MQTTTargetFactory));
|
||||||
registry.register(ChannelTargetType::Nats.as_str(), Box::new(NATSTargetFactory));
|
registry.register(ChannelTargetType::Nats.as_str(), Box::new(NATSTargetFactory));
|
||||||
registry.register(ChannelTargetType::Pulsar.as_str(), Box::new(PulsarTargetFactory));
|
registry.register(ChannelTargetType::Pulsar.as_str(), Box::new(PulsarTargetFactory));
|
||||||
|
registry.register(ChannelTargetType::Kafka.as_str(), Box::new(KafkaTargetFactory));
|
||||||
|
|
||||||
registry
|
registry
|
||||||
}
|
}
|
||||||
@@ -197,8 +200,8 @@ impl AuditRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !errors.is_empty() {
|
if let Some(error) = errors.into_iter().next() {
|
||||||
return Err(AuditError::Target(errors.into_iter().next().unwrap()));
|
return Err(AuditError::Target(error));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ impl InternodeMetrics {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.sent_bytes_total.fetch_add(bytes, Ordering::Relaxed);
|
self.sent_bytes_total.fetch_add(bytes, Ordering::Relaxed);
|
||||||
counter!("rustfs.internode.sent.bytes.total").increment(bytes);
|
counter!("rustfs_system_network_internode_sent_bytes_total").increment(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn record_recv_bytes(&self, bytes: usize) {
|
pub fn record_recv_bytes(&self, bytes: usize) {
|
||||||
@@ -60,22 +60,22 @@ impl InternodeMetrics {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.recv_bytes_total.fetch_add(bytes, Ordering::Relaxed);
|
self.recv_bytes_total.fetch_add(bytes, Ordering::Relaxed);
|
||||||
counter!("rustfs.internode.recv.bytes.total").increment(bytes);
|
counter!("rustfs_system_network_internode_recv_bytes_total").increment(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn record_outgoing_request(&self) {
|
pub fn record_outgoing_request(&self) {
|
||||||
self.outgoing_requests_total.fetch_add(1, Ordering::Relaxed);
|
self.outgoing_requests_total.fetch_add(1, Ordering::Relaxed);
|
||||||
counter!("rustfs.internode.requests.outgoing.total").increment(1);
|
counter!("rustfs_system_network_internode_requests_outgoing_total").increment(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn record_incoming_request(&self) {
|
pub fn record_incoming_request(&self) {
|
||||||
self.incoming_requests_total.fetch_add(1, Ordering::Relaxed);
|
self.incoming_requests_total.fetch_add(1, Ordering::Relaxed);
|
||||||
counter!("rustfs.internode.requests.incoming.total").increment(1);
|
counter!("rustfs_system_network_internode_requests_incoming_total").increment(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn record_error(&self) {
|
pub fn record_error(&self) {
|
||||||
self.errors_total.fetch_add(1, Ordering::Relaxed);
|
self.errors_total.fetch_add(1, Ordering::Relaxed);
|
||||||
counter!("rustfs.internode.errors.total").increment(1);
|
counter!("rustfs_system_network_internode_errors_total").increment(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn record_dial_result(&self, duration: Duration, success: bool) {
|
pub fn record_dial_result(&self, duration: Duration, success: bool) {
|
||||||
@@ -83,11 +83,11 @@ impl InternodeMetrics {
|
|||||||
self.dial_total_time_nanos.fetch_add(elapsed_nanos, Ordering::Relaxed);
|
self.dial_total_time_nanos.fetch_add(elapsed_nanos, Ordering::Relaxed);
|
||||||
let samples = self.dial_samples_total.fetch_add(1, Ordering::Relaxed) + 1;
|
let samples = self.dial_samples_total.fetch_add(1, Ordering::Relaxed) + 1;
|
||||||
let total = self.dial_total_time_nanos.load(Ordering::Relaxed);
|
let total = self.dial_total_time_nanos.load(Ordering::Relaxed);
|
||||||
gauge!("rustfs.internode.dial.avg_time.nanos").set(total as f64 / samples as f64);
|
gauge!("rustfs_system_network_internode_dial_avg_time_nanos").set(total as f64 / samples as f64);
|
||||||
|
|
||||||
if !success {
|
if !success {
|
||||||
self.dial_errors_total.fetch_add(1, Ordering::Relaxed);
|
self.dial_errors_total.fetch_add(1, Ordering::Relaxed);
|
||||||
counter!("rustfs.internode.dial.errors.total").increment(1);
|
counter!("rustfs_system_network_internode_dial_errors_total").increment(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let now_ms = SystemTime::now()
|
let now_ms = SystemTime::now()
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ Examples:
|
|||||||
- `RUSTFS_ADDRESS`
|
- `RUSTFS_ADDRESS`
|
||||||
- `RUSTFS_VOLUMES`
|
- `RUSTFS_VOLUMES`
|
||||||
- `RUSTFS_LICENSE`
|
- `RUSTFS_LICENSE`
|
||||||
|
- `RUSTFS_LICENSE_PUBLIC_KEY`
|
||||||
|
|
||||||
Current guidance:
|
Current guidance:
|
||||||
- Prefer module-specific names only when they are not top-level product configuration.
|
- Prefer module-specific names only when they are not top-level product configuration.
|
||||||
@@ -51,6 +52,16 @@ Current guidance:
|
|||||||
- `RUSTFS_ENABLE_HEAL` -> `RUSTFS_HEAL_ENABLED`
|
- `RUSTFS_ENABLE_HEAL` -> `RUSTFS_HEAL_ENABLED`
|
||||||
- `RUSTFS_DATA_SCANNER_START_DELAY_SECS` -> `RUSTFS_SCANNER_START_DELAY_SECS`
|
- `RUSTFS_DATA_SCANNER_START_DELAY_SECS` -> `RUSTFS_SCANNER_START_DELAY_SECS`
|
||||||
|
|
||||||
|
## License environment variables
|
||||||
|
|
||||||
|
- `RUSTFS_LICENSE` contains the signed license token.
|
||||||
|
- `RUSTFS_LICENSE_PUBLIC_KEY` contains the RSA public key used to verify signed license tokens.
|
||||||
|
|
||||||
|
## CORS environment variables
|
||||||
|
|
||||||
|
- `RUSTFS_CORS_ALLOWED_ORIGINS` defaults to empty, so the S3 endpoint emits no generic CORS headers unless configured. Set `*` for wildcard origins without credentials, or a comma-separated allow-list for credentialed explicit origins.
|
||||||
|
- `RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS` defaults to `*` for the console service.
|
||||||
|
|
||||||
## Scanner environment aliases
|
## Scanner environment aliases
|
||||||
|
|
||||||
- `RUSTFS_SCANNER_SPEED` (canonical, also accepts `MINIO_SCANNER_SPEED`)
|
- `RUSTFS_SCANNER_SPEED` (canonical, also accepts `MINIO_SCANNER_SPEED`)
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Kafka Environment Variables
|
||||||
|
pub const ENV_AUDIT_KAFKA_ENABLE: &str = "RUSTFS_AUDIT_KAFKA_ENABLE";
|
||||||
|
pub const ENV_AUDIT_KAFKA_BROKERS: &str = "RUSTFS_AUDIT_KAFKA_BROKERS";
|
||||||
|
pub const ENV_AUDIT_KAFKA_TOPIC: &str = "RUSTFS_AUDIT_KAFKA_TOPIC";
|
||||||
|
pub const ENV_AUDIT_KAFKA_ACKS: &str = "RUSTFS_AUDIT_KAFKA_ACKS";
|
||||||
|
pub const ENV_AUDIT_KAFKA_TLS_ENABLE: &str = "RUSTFS_AUDIT_KAFKA_TLS_ENABLE";
|
||||||
|
pub const ENV_AUDIT_KAFKA_TLS_CA: &str = "RUSTFS_AUDIT_KAFKA_TLS_CA";
|
||||||
|
pub const ENV_AUDIT_KAFKA_TLS_CLIENT_CERT: &str = "RUSTFS_AUDIT_KAFKA_TLS_CLIENT_CERT";
|
||||||
|
pub const ENV_AUDIT_KAFKA_TLS_CLIENT_KEY: &str = "RUSTFS_AUDIT_KAFKA_TLS_CLIENT_KEY";
|
||||||
|
pub const ENV_AUDIT_KAFKA_QUEUE_DIR: &str = "RUSTFS_AUDIT_KAFKA_QUEUE_DIR";
|
||||||
|
pub const ENV_AUDIT_KAFKA_QUEUE_LIMIT: &str = "RUSTFS_AUDIT_KAFKA_QUEUE_LIMIT";
|
||||||
|
|
||||||
|
pub const ENV_AUDIT_KAFKA_KEYS: &[&str; 10] = &[
|
||||||
|
ENV_AUDIT_KAFKA_ENABLE,
|
||||||
|
ENV_AUDIT_KAFKA_BROKERS,
|
||||||
|
ENV_AUDIT_KAFKA_TOPIC,
|
||||||
|
ENV_AUDIT_KAFKA_ACKS,
|
||||||
|
ENV_AUDIT_KAFKA_TLS_ENABLE,
|
||||||
|
ENV_AUDIT_KAFKA_TLS_CA,
|
||||||
|
ENV_AUDIT_KAFKA_TLS_CLIENT_CERT,
|
||||||
|
ENV_AUDIT_KAFKA_TLS_CLIENT_KEY,
|
||||||
|
ENV_AUDIT_KAFKA_QUEUE_DIR,
|
||||||
|
ENV_AUDIT_KAFKA_QUEUE_LIMIT,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// A list of all valid configuration keys for a Kafka audit target.
|
||||||
|
pub const AUDIT_KAFKA_KEYS: &[&str] = &[
|
||||||
|
crate::ENABLE_KEY,
|
||||||
|
crate::KAFKA_BROKERS,
|
||||||
|
crate::KAFKA_TOPIC,
|
||||||
|
crate::KAFKA_ACKS,
|
||||||
|
crate::KAFKA_TLS_ENABLE,
|
||||||
|
crate::KAFKA_TLS_CA,
|
||||||
|
crate::KAFKA_TLS_CLIENT_CERT,
|
||||||
|
crate::KAFKA_TLS_CLIENT_KEY,
|
||||||
|
crate::KAFKA_QUEUE_DIR,
|
||||||
|
crate::KAFKA_QUEUE_LIMIT,
|
||||||
|
crate::COMMENT_KEY,
|
||||||
|
];
|
||||||
@@ -16,11 +16,13 @@
|
|||||||
//! This module defines the configuration for audit systems, including
|
//! This module defines the configuration for audit systems, including
|
||||||
//! webhook and MQTT audit-related settings.
|
//! webhook and MQTT audit-related settings.
|
||||||
|
|
||||||
|
mod kafka;
|
||||||
mod mqtt;
|
mod mqtt;
|
||||||
mod nats;
|
mod nats;
|
||||||
mod pulsar;
|
mod pulsar;
|
||||||
mod webhook;
|
mod webhook;
|
||||||
|
|
||||||
|
pub use kafka::*;
|
||||||
pub use mqtt::*;
|
pub use mqtt::*;
|
||||||
pub use nats::*;
|
pub use nats::*;
|
||||||
pub use pulsar::*;
|
pub use pulsar::*;
|
||||||
@@ -33,6 +35,7 @@ pub const AUDIT_PREFIX: &str = "audit";
|
|||||||
pub const AUDIT_ROUTE_PREFIX: &str = const_str::concat!(AUDIT_PREFIX, DEFAULT_DELIMITER);
|
pub const AUDIT_ROUTE_PREFIX: &str = const_str::concat!(AUDIT_PREFIX, DEFAULT_DELIMITER);
|
||||||
|
|
||||||
pub const AUDIT_WEBHOOK_SUB_SYS: &str = "audit_webhook";
|
pub const AUDIT_WEBHOOK_SUB_SYS: &str = "audit_webhook";
|
||||||
|
pub const AUDIT_KAFKA_SUB_SYS: &str = "audit_kafka";
|
||||||
pub const AUDIT_MQTT_SUB_SYS: &str = "audit_mqtt";
|
pub const AUDIT_MQTT_SUB_SYS: &str = "audit_mqtt";
|
||||||
pub const AUDIT_NATS_SUB_SYS: &str = "audit_nats";
|
pub const AUDIT_NATS_SUB_SYS: &str = "audit_nats";
|
||||||
pub const AUDIT_PULSAR_SUB_SYS: &str = "audit_pulsar";
|
pub const AUDIT_PULSAR_SUB_SYS: &str = "audit_pulsar";
|
||||||
@@ -40,6 +43,7 @@ pub const AUDIT_PULSAR_SUB_SYS: &str = "audit_pulsar";
|
|||||||
pub const AUDIT_STORE_EXTENSION: &str = ".audit";
|
pub const AUDIT_STORE_EXTENSION: &str = ".audit";
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub const AUDIT_SUB_SYSTEMS: &[&str] = &[
|
pub const AUDIT_SUB_SYSTEMS: &[&str] = &[
|
||||||
|
AUDIT_KAFKA_SUB_SYS,
|
||||||
AUDIT_MQTT_SUB_SYS,
|
AUDIT_MQTT_SUB_SYS,
|
||||||
AUDIT_NATS_SUB_SYS,
|
AUDIT_NATS_SUB_SYS,
|
||||||
AUDIT_PULSAR_SUB_SYS,
|
AUDIT_PULSAR_SUB_SYS,
|
||||||
|
|||||||
@@ -131,24 +131,30 @@ pub const ENV_RUSTFS_ADDRESS: &str = "RUSTFS_ADDRESS";
|
|||||||
/// Environment variable for server volumes.
|
/// Environment variable for server volumes.
|
||||||
pub const ENV_RUSTFS_VOLUMES: &str = "RUSTFS_VOLUMES";
|
pub const ENV_RUSTFS_VOLUMES: &str = "RUSTFS_VOLUMES";
|
||||||
|
|
||||||
|
/// Environment variable to explicitly bypass local physical disk independence checks.
|
||||||
|
pub const ENV_UNSAFE_BYPASS_DISK_CHECK: &str = "RUSTFS_UNSAFE_BYPASS_DISK_CHECK";
|
||||||
|
|
||||||
|
/// Compatibility alias used by legacy MinIO CI pipelines.
|
||||||
|
///
|
||||||
|
/// RustFS keeps this alias for backward compatibility only. Prefer
|
||||||
|
/// `ENV_UNSAFE_BYPASS_DISK_CHECK` for explicit bypass control.
|
||||||
|
pub const ENV_MINIO_CI: &str = "MINIO_CI";
|
||||||
|
|
||||||
|
/// Default flag value for bypassing local physical disk independence checks.
|
||||||
|
pub const DEFAULT_UNSAFE_BYPASS_DISK_CHECK: bool = false;
|
||||||
|
|
||||||
/// Environment variable for server access key.
|
/// Environment variable for server access key.
|
||||||
pub const ENV_RUSTFS_ACCESS_KEY: &str = "RUSTFS_ACCESS_KEY";
|
pub const ENV_RUSTFS_ACCESS_KEY: &str = "RUSTFS_ACCESS_KEY";
|
||||||
|
|
||||||
/// Environment variable for server access key file.
|
/// Environment variable for server access key file.
|
||||||
pub const ENV_RUSTFS_ACCESS_KEY_FILE: &str = "RUSTFS_ACCESS_KEY_FILE";
|
pub const ENV_RUSTFS_ACCESS_KEY_FILE: &str = "RUSTFS_ACCESS_KEY_FILE";
|
||||||
|
|
||||||
/// Environment variable for server root user.
|
|
||||||
pub const ENV_RUSTFS_ROOT_USER: &str = "RUSTFS_ROOT_USER";
|
|
||||||
|
|
||||||
/// Environment variable for server secret key.
|
/// Environment variable for server secret key.
|
||||||
pub const ENV_RUSTFS_SECRET_KEY: &str = "RUSTFS_SECRET_KEY";
|
pub const ENV_RUSTFS_SECRET_KEY: &str = "RUSTFS_SECRET_KEY";
|
||||||
|
|
||||||
/// Environment variable for server secret key file.
|
/// Environment variable for server secret key file.
|
||||||
pub const ENV_RUSTFS_SECRET_KEY_FILE: &str = "RUSTFS_SECRET_KEY_FILE";
|
pub const ENV_RUSTFS_SECRET_KEY_FILE: &str = "RUSTFS_SECRET_KEY_FILE";
|
||||||
|
|
||||||
/// Environment variable for server root password.
|
|
||||||
pub const ENV_RUSTFS_ROOT_PASSWORD: &str = "RUSTFS_ROOT_PASSWORD";
|
|
||||||
|
|
||||||
/// Environment variable for server OBS endpoint.
|
/// Environment variable for server OBS endpoint.
|
||||||
pub const ENV_RUSTFS_OBS_ENDPOINT: &str = "RUSTFS_OBS_ENDPOINT";
|
pub const ENV_RUSTFS_OBS_ENDPOINT: &str = "RUSTFS_OBS_ENDPOINT";
|
||||||
|
|
||||||
@@ -223,6 +229,9 @@ pub const ENV_RUSTFS_REGION: &str = "RUSTFS_REGION";
|
|||||||
/// Environment variable for server license.
|
/// Environment variable for server license.
|
||||||
pub const ENV_RUSTFS_LICENSE: &str = "RUSTFS_LICENSE";
|
pub const ENV_RUSTFS_LICENSE: &str = "RUSTFS_LICENSE";
|
||||||
|
|
||||||
|
/// Environment variable for the RSA public key used to verify server licenses.
|
||||||
|
pub const ENV_RUSTFS_LICENSE_PUBLIC_KEY: &str = "RUSTFS_LICENSE_PUBLIC_KEY";
|
||||||
|
|
||||||
/// Default log filename for rustfs
|
/// Default log filename for rustfs
|
||||||
/// This is the default log filename for rustfs.
|
/// This is the default log filename for rustfs.
|
||||||
/// It is used to store the logs of the application.
|
/// It is used to store the logs of the application.
|
||||||
@@ -284,9 +293,9 @@ pub const DEFAULT_OBS_LOGS_EXPORT_ENABLED: bool = true;
|
|||||||
|
|
||||||
/// Default profiling export enabled
|
/// Default profiling export enabled
|
||||||
/// It is used to enable or disable exporting profiles
|
/// It is used to enable or disable exporting profiles
|
||||||
/// Default value: true
|
/// Default value: false
|
||||||
/// Environment variable: RUSTFS_OBS_PROFILING_EXPORT_ENABLED
|
/// Environment variable: RUSTFS_OBS_PROFILING_EXPORT_ENABLED
|
||||||
pub const DEFAULT_OBS_PROFILING_EXPORT_ENABLED: bool = true;
|
pub const DEFAULT_OBS_PROFILING_EXPORT_ENABLED: bool = false;
|
||||||
|
|
||||||
/// Default log local logging enabled for rustfs
|
/// Default log local logging enabled for rustfs
|
||||||
/// This is the default log local logging enabled for rustfs.
|
/// This is the default log local logging enabled for rustfs.
|
||||||
@@ -336,6 +345,7 @@ mod tests {
|
|||||||
fn test_environment_constants() {
|
fn test_environment_constants() {
|
||||||
// Test environment related constants
|
// Test environment related constants
|
||||||
assert_eq!(ENVIRONMENT, "production");
|
assert_eq!(ENVIRONMENT, "production");
|
||||||
|
assert_eq!(ENV_RUSTFS_LICENSE_PUBLIC_KEY, "RUSTFS_LICENSE_PUBLIC_KEY");
|
||||||
assert!(
|
assert!(
|
||||||
["development", "staging", "production", "test"].contains(&ENVIRONMENT),
|
["development", "staging", "production", "test"].contains(&ENVIRONMENT),
|
||||||
"Environment should be a standard environment name"
|
"Environment should be a standard environment name"
|
||||||
|
|||||||
@@ -17,16 +17,21 @@
|
|||||||
pub const ENV_CORS_ALLOWED_ORIGINS: &str = "RUSTFS_CORS_ALLOWED_ORIGINS";
|
pub const ENV_CORS_ALLOWED_ORIGINS: &str = "RUSTFS_CORS_ALLOWED_ORIGINS";
|
||||||
|
|
||||||
/// Default CORS allowed origins for the endpoint service
|
/// Default CORS allowed origins for the endpoint service
|
||||||
/// Comes from the console service default
|
/// Empty means the S3 endpoint emits no generic CORS headers unless configured.
|
||||||
/// See DEFAULT_CONSOLE_CORS_ALLOWED_ORIGINS
|
pub const DEFAULT_CORS_ALLOWED_ORIGINS: &str = "";
|
||||||
pub const DEFAULT_CORS_ALLOWED_ORIGINS: &str = DEFAULT_CONSOLE_CORS_ALLOWED_ORIGINS;
|
|
||||||
|
|
||||||
/// CORS allowed origins for the console service
|
/// CORS allowed origins for the console service
|
||||||
/// Comma-separated list of origins or "*" for all origins
|
/// Comma-separated list of origins or "*" for all origins
|
||||||
pub const ENV_CONSOLE_CORS_ALLOWED_ORIGINS: &str = "RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS";
|
pub const ENV_CONSOLE_CORS_ALLOWED_ORIGINS: &str = "RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS";
|
||||||
|
|
||||||
/// Default CORS allowed origins for the console service
|
/// Default CORS allowed origins for the console service.
|
||||||
pub const DEFAULT_CONSOLE_CORS_ALLOWED_ORIGINS: &str = "*";
|
///
|
||||||
|
/// Empty string means same-origin only — no `Access-Control-Allow-Origin`
|
||||||
|
/// header is emitted, so browsers will not allow cross-origin reads of
|
||||||
|
/// console responses by default. Operators that need cross-origin access set
|
||||||
|
/// `RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS` to a comma-separated allow-list, or
|
||||||
|
/// to `*` to keep the previous permissive behavior.
|
||||||
|
pub const DEFAULT_CONSOLE_CORS_ALLOWED_ORIGINS: &str = "";
|
||||||
|
|
||||||
/// Enable or disable the console service
|
/// Enable or disable the console service
|
||||||
pub const ENV_CONSOLE_ENABLE: &str = "RUSTFS_CONSOLE_ENABLE";
|
pub const ENV_CONSOLE_ENABLE: &str = "RUSTFS_CONSOLE_ENABLE";
|
||||||
@@ -89,3 +94,20 @@ pub const ENV_UPDATE_CHECK: &str = "RUSTFS_CHECK_UPDATE";
|
|||||||
|
|
||||||
/// Default value for update toggle
|
/// Default value for update toggle
|
||||||
pub const DEFAULT_UPDATE_CHECK: bool = true;
|
pub const DEFAULT_UPDATE_CHECK: bool = true;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn endpoint_cors_default_is_restrictive() {
|
||||||
|
assert_eq!(ENV_CORS_ALLOWED_ORIGINS, "RUSTFS_CORS_ALLOWED_ORIGINS");
|
||||||
|
assert_eq!(DEFAULT_CORS_ALLOWED_ORIGINS, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn console_cors_default_is_same_origin_only() {
|
||||||
|
assert_eq!(ENV_CONSOLE_CORS_ALLOWED_ORIGINS, "RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS");
|
||||||
|
assert_eq!(DEFAULT_CONSOLE_CORS_ALLOWED_ORIGINS, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ pub const DEFAULT_DRIVE_WALKDIR_TIMEOUT_SECS: u64 = 5;
|
|||||||
pub const ENV_DRIVE_WALKDIR_STALL_TIMEOUT_SECS: &str = "RUSTFS_DRIVE_WALKDIR_STALL_TIMEOUT_SECS";
|
pub const ENV_DRIVE_WALKDIR_STALL_TIMEOUT_SECS: &str = "RUSTFS_DRIVE_WALKDIR_STALL_TIMEOUT_SECS";
|
||||||
pub const DEFAULT_DRIVE_WALKDIR_STALL_TIMEOUT_SECS: u64 = 5;
|
pub const DEFAULT_DRIVE_WALKDIR_STALL_TIMEOUT_SECS: u64 = 5;
|
||||||
|
|
||||||
|
/// Interval in seconds between active health probes for local and remote drives.
|
||||||
|
pub const ENV_DRIVE_ACTIVE_CHECK_INTERVAL_SECS: &str = "RUSTFS_DRIVE_ACTIVE_CHECK_INTERVAL_SECS";
|
||||||
|
pub const DEFAULT_DRIVE_ACTIVE_CHECK_INTERVAL_SECS: u64 = 15;
|
||||||
|
|
||||||
|
/// Timeout in seconds for a single active health probe.
|
||||||
|
pub const ENV_DRIVE_ACTIVE_CHECK_TIMEOUT_SECS: &str = "RUSTFS_DRIVE_ACTIVE_CHECK_TIMEOUT_SECS";
|
||||||
|
pub const DEFAULT_DRIVE_ACTIVE_CHECK_TIMEOUT_SECS: u64 = 5;
|
||||||
|
|
||||||
/// Number of consecutive failures before a suspect drive is classified as offline.
|
/// Number of consecutive failures before a suspect drive is classified as offline.
|
||||||
pub const ENV_DRIVE_SUSPECT_FAILURE_THRESHOLD: &str = "RUSTFS_DRIVE_SUSPECT_FAILURE_THRESHOLD";
|
pub const ENV_DRIVE_SUSPECT_FAILURE_THRESHOLD: &str = "RUSTFS_DRIVE_SUSPECT_FAILURE_THRESHOLD";
|
||||||
pub const DEFAULT_DRIVE_SUSPECT_FAILURE_THRESHOLD: u64 = 2;
|
pub const DEFAULT_DRIVE_SUSPECT_FAILURE_THRESHOLD: u64 = 2;
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ pub const RUSTFS_WEBHOOK_SKIP_TLS_VERIFY_DEFAULT: bool = false;
|
|||||||
pub const ENABLE_KEY: &str = "enable";
|
pub const ENABLE_KEY: &str = "enable";
|
||||||
pub const COMMENT_KEY: &str = "comment";
|
pub const COMMENT_KEY: &str = "comment";
|
||||||
|
|
||||||
|
/// Global switch for enabling the audit module.
|
||||||
|
pub const ENV_AUDIT_ENABLE: &str = "RUSTFS_AUDIT_ENABLE";
|
||||||
|
/// Global switch for enabling the notify module.
|
||||||
|
pub const ENV_NOTIFY_ENABLE: &str = "RUSTFS_NOTIFY_ENABLE";
|
||||||
|
/// Default global audit switch (disabled by default).
|
||||||
|
pub const DEFAULT_AUDIT_ENABLE: bool = false;
|
||||||
|
/// Default global notify switch (disabled by default).
|
||||||
|
pub const DEFAULT_NOTIFY_ENABLE: bool = false;
|
||||||
|
|
||||||
/// Medium-drawn lines separator
|
/// Medium-drawn lines separator
|
||||||
/// This is used to separate words in environment variable names.
|
/// This is used to separate words in environment variable names.
|
||||||
pub const ENV_WORD_DELIMITER_DASH: &str = "-";
|
pub const ENV_WORD_DELIMITER_DASH: &str = "-";
|
||||||
@@ -290,4 +299,10 @@ mod tests {
|
|||||||
assert!(state.is_disabled());
|
assert!(state.is_disabled());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_global_audit_notify_switch_constants() {
|
||||||
|
assert_eq!(ENV_AUDIT_ENABLE, "RUSTFS_AUDIT_ENABLE");
|
||||||
|
assert_eq!(ENV_NOTIFY_ENABLE, "RUSTFS_NOTIFY_ENABLE");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
/// Enable or disable public `/health` and `/health/ready` endpoints.
|
||||||
|
/// When disabled, the routes are not registered and return 404.
|
||||||
|
pub const ENV_HEALTH_ENDPOINT_ENABLE: &str = "RUSTFS_HEALTH_ENDPOINT_ENABLE";
|
||||||
|
pub const DEFAULT_HEALTH_ENDPOINT_ENABLE: bool = true;
|
||||||
|
|
||||||
|
/// Cache TTL for storage readiness runtime-state evaluation (milliseconds).
|
||||||
|
/// This reduces storage-layer pressure when probes are called at high frequency.
|
||||||
|
pub const ENV_HEALTH_READINESS_CACHE_TTL_MS: &str = "RUSTFS_HEALTH_READINESS_CACHE_TTL_MS";
|
||||||
|
pub const DEFAULT_HEALTH_READINESS_CACHE_TTL_MS: u64 = 1000;
|
||||||
|
|
||||||
|
/// Enable minimal health payload mode for GET `/health*` responses.
|
||||||
|
/// When enabled, only `status` and `ready` fields are returned.
|
||||||
|
pub const ENV_HEALTH_MINIMAL_RESPONSE_ENABLE: &str = "RUSTFS_HEALTH_MINIMAL_RESPONSE_ENABLE";
|
||||||
|
pub const DEFAULT_HEALTH_MINIMAL_RESPONSE_ENABLE: bool = false;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
/// Timeout for establishing a new internode gRPC connection.
|
||||||
|
pub const ENV_INTERNODE_CONNECT_TIMEOUT_SECS: &str = "RUSTFS_INTERNODE_CONNECT_TIMEOUT_SECS";
|
||||||
|
pub const DEFAULT_INTERNODE_CONNECT_TIMEOUT_SECS: u64 = 3;
|
||||||
|
|
||||||
|
/// TCP keepalive interval for internode gRPC channels.
|
||||||
|
pub const ENV_INTERNODE_TCP_KEEPALIVE_SECS: &str = "RUSTFS_INTERNODE_TCP_KEEPALIVE_SECS";
|
||||||
|
pub const DEFAULT_INTERNODE_TCP_KEEPALIVE_SECS: u64 = 10;
|
||||||
|
|
||||||
|
/// HTTP/2 keepalive interval for internode gRPC channels.
|
||||||
|
pub const ENV_INTERNODE_HTTP2_KEEPALIVE_INTERVAL_SECS: &str = "RUSTFS_INTERNODE_HTTP2_KEEPALIVE_INTERVAL_SECS";
|
||||||
|
pub const DEFAULT_INTERNODE_HTTP2_KEEPALIVE_INTERVAL_SECS: u64 = 5;
|
||||||
|
|
||||||
|
/// HTTP/2 keepalive timeout for internode gRPC channels.
|
||||||
|
pub const ENV_INTERNODE_HTTP2_KEEPALIVE_TIMEOUT_SECS: &str = "RUSTFS_INTERNODE_HTTP2_KEEPALIVE_TIMEOUT_SECS";
|
||||||
|
pub const DEFAULT_INTERNODE_HTTP2_KEEPALIVE_TIMEOUT_SECS: u64 = 3;
|
||||||
|
|
||||||
|
/// Overall timeout for a single internode gRPC request.
|
||||||
|
pub const ENV_INTERNODE_RPC_TIMEOUT_SECS: &str = "RUSTFS_INTERNODE_RPC_TIMEOUT_SECS";
|
||||||
|
pub const DEFAULT_INTERNODE_RPC_TIMEOUT_SECS: u64 = 10;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn internode_timeout_defaults_stay_in_expected_bounds() {
|
||||||
|
assert_eq!(DEFAULT_INTERNODE_CONNECT_TIMEOUT_SECS, 3);
|
||||||
|
assert_eq!(DEFAULT_INTERNODE_TCP_KEEPALIVE_SECS, 10);
|
||||||
|
assert_eq!(DEFAULT_INTERNODE_HTTP2_KEEPALIVE_INTERVAL_SECS, 5);
|
||||||
|
assert_eq!(DEFAULT_INTERNODE_HTTP2_KEEPALIVE_TIMEOUT_SECS, 3);
|
||||||
|
assert_eq!(DEFAULT_INTERNODE_RPC_TIMEOUT_SECS, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn internode_timeout_env_names_are_stable() {
|
||||||
|
assert_eq!(ENV_INTERNODE_CONNECT_TIMEOUT_SECS, "RUSTFS_INTERNODE_CONNECT_TIMEOUT_SECS");
|
||||||
|
assert_eq!(ENV_INTERNODE_TCP_KEEPALIVE_SECS, "RUSTFS_INTERNODE_TCP_KEEPALIVE_SECS");
|
||||||
|
assert_eq!(
|
||||||
|
ENV_INTERNODE_HTTP2_KEEPALIVE_INTERVAL_SECS,
|
||||||
|
"RUSTFS_INTERNODE_HTTP2_KEEPALIVE_INTERVAL_SECS"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ENV_INTERNODE_HTTP2_KEEPALIVE_TIMEOUT_SECS,
|
||||||
|
"RUSTFS_INTERNODE_HTTP2_KEEPALIVE_TIMEOUT_SECS"
|
||||||
|
);
|
||||||
|
assert_eq!(ENV_INTERNODE_RPC_TIMEOUT_SECS, "RUSTFS_INTERNODE_RPC_TIMEOUT_SECS");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,8 @@ pub(crate) mod console;
|
|||||||
pub(crate) mod drive;
|
pub(crate) mod drive;
|
||||||
pub(crate) mod env;
|
pub(crate) mod env;
|
||||||
pub(crate) mod heal;
|
pub(crate) mod heal;
|
||||||
|
pub(crate) mod health;
|
||||||
|
pub(crate) mod internode;
|
||||||
pub(crate) mod object;
|
pub(crate) mod object;
|
||||||
pub(crate) mod oidc;
|
pub(crate) mod oidc;
|
||||||
pub(crate) mod profiler;
|
pub(crate) mod profiler;
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ pub const ENV_TRUSTED_PROXY_ENABLED: &str = "RUSTFS_TRUSTED_PROXY_ENABLED";
|
|||||||
/// Trusted proxy middleware is enabled by default.
|
/// Trusted proxy middleware is enabled by default.
|
||||||
pub const DEFAULT_TRUSTED_PROXY_ENABLED: bool = true;
|
pub const DEFAULT_TRUSTED_PROXY_ENABLED: bool = true;
|
||||||
|
|
||||||
|
/// Environment variable to select the trusted proxy implementation.
|
||||||
|
pub const ENV_TRUSTED_PROXY_IMPLEMENTATION: &str = "RUSTFS_TRUSTED_PROXY_IMPLEMENTATION";
|
||||||
|
/// The simplified implementation is used by default.
|
||||||
|
pub const DEFAULT_TRUSTED_PROXY_IMPLEMENTATION: &str = "simple";
|
||||||
|
|
||||||
/// Environment variable for the proxy validation mode.
|
/// Environment variable for the proxy validation mode.
|
||||||
pub const ENV_TRUSTED_PROXY_VALIDATION_MODE: &str = "RUSTFS_TRUSTED_PROXY_VALIDATION_MODE";
|
pub const ENV_TRUSTED_PROXY_VALIDATION_MODE: &str = "RUSTFS_TRUSTED_PROXY_VALIDATION_MODE";
|
||||||
/// Default validation mode is "hop_by_hop".
|
/// Default validation mode is "hop_by_hop".
|
||||||
|
|||||||
@@ -59,9 +59,31 @@ pub const DEFAULT_RUNTIME_DIAL9_ROTATION_COUNT: usize = 10;
|
|||||||
pub const DEFAULT_RUNTIME_DIAL9_SAMPLING_RATE: f64 = 1.0; // 100% sampling
|
pub const DEFAULT_RUNTIME_DIAL9_SAMPLING_RATE: f64 = 1.0; // 100% sampling
|
||||||
// Note: S3 bucket/prefix have no default; absence means upload is disabled (modeled as Option<String>)
|
// Note: S3 bucket/prefix have no default; absence means upload is disabled (modeled as Option<String>)
|
||||||
|
|
||||||
/// Threshold for small object seek support in megabytes.
|
// Allocator reclaim configuration
|
||||||
|
pub const ENV_ALLOCATOR_RECLAIM_ENABLED: &str = "RUSTFS_ALLOCATOR_RECLAIM_ENABLED";
|
||||||
|
pub const ENV_ALLOCATOR_RECLAIM_INTERVAL_SECS: &str = "RUSTFS_ALLOCATOR_RECLAIM_INTERVAL_SECS";
|
||||||
|
pub const ENV_ALLOCATOR_RECLAIM_FORCE: &str = "RUSTFS_ALLOCATOR_RECLAIM_FORCE";
|
||||||
|
pub const ENV_ALLOCATOR_RECLAIM_IDLE_INTERVALS: &str = "RUSTFS_ALLOCATOR_RECLAIM_IDLE_INTERVALS";
|
||||||
|
pub const DEFAULT_ALLOCATOR_RECLAIM_ENABLED: bool = false;
|
||||||
|
pub const DEFAULT_ALLOCATOR_RECLAIM_INTERVAL_SECS: u64 = 30;
|
||||||
|
pub const DEFAULT_ALLOCATOR_RECLAIM_FORCE: bool = true;
|
||||||
|
pub const DEFAULT_ALLOCATOR_RECLAIM_IDLE_INTERVALS: u64 = 3;
|
||||||
|
|
||||||
|
// File page-cache reclaim configuration
|
||||||
|
pub const ENV_OBJECT_FILE_CACHE_RECLAIM_WRITE_ENABLE: &str = "RUSTFS_OBJECT_FILE_CACHE_RECLAIM_WRITE_ENABLE";
|
||||||
|
pub const ENV_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE: &str = "RUSTFS_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE";
|
||||||
|
pub const ENV_OBJECT_FILE_CACHE_RECLAIM_THRESHOLD: &str = "RUSTFS_OBJECT_FILE_CACHE_RECLAIM_THRESHOLD";
|
||||||
|
pub const DEFAULT_OBJECT_FILE_CACHE_RECLAIM_WRITE_ENABLE: bool = false;
|
||||||
|
pub const DEFAULT_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE: bool = false;
|
||||||
|
pub const DEFAULT_OBJECT_FILE_CACHE_RECLAIM_THRESHOLD: usize = 4 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// Threshold for small object seek support in bytes.
|
||||||
///
|
///
|
||||||
/// When an object is smaller than this size, rustfs will provide seek support.
|
/// When an object response is smaller than this size, rustfs may provide
|
||||||
|
/// in-memory seek support. Runtime GET logic also enforces a hard safety cap
|
||||||
|
/// (`64 MiB`) to prevent large-download memory spikes even if this threshold
|
||||||
|
/// is configured higher.
|
||||||
///
|
///
|
||||||
/// Default is set to 10MB.
|
/// Default is set to 10 MiB.
|
||||||
|
pub const ENV_OBJECT_SEEK_SUPPORT_THRESHOLD: &str = "RUSTFS_OBJECT_SEEK_SUPPORT_THRESHOLD";
|
||||||
pub const DEFAULT_OBJECT_SEEK_SUPPORT_THRESHOLD: usize = 10 * 1024 * 1024;
|
pub const DEFAULT_OBJECT_SEEK_SUPPORT_THRESHOLD: usize = 10 * 1024 * 1024;
|
||||||
|
|||||||
@@ -40,6 +40,15 @@ pub const MQTT_TLS_CLIENT_CERT: &str = "tls_client_cert";
|
|||||||
pub const MQTT_TLS_CLIENT_KEY: &str = "tls_client_key";
|
pub const MQTT_TLS_CLIENT_KEY: &str = "tls_client_key";
|
||||||
pub const MQTT_TLS_TRUST_LEAF_AS_CA: &str = "tls_trust_leaf_as_ca";
|
pub const MQTT_TLS_TRUST_LEAF_AS_CA: &str = "tls_trust_leaf_as_ca";
|
||||||
pub const MQTT_WS_PATH_ALLOWLIST: &str = "ws_path_allowlist";
|
pub const MQTT_WS_PATH_ALLOWLIST: &str = "ws_path_allowlist";
|
||||||
|
pub const KAFKA_BROKERS: &str = "brokers";
|
||||||
|
pub const KAFKA_TOPIC: &str = "topic";
|
||||||
|
pub const KAFKA_ACKS: &str = "acks";
|
||||||
|
pub const KAFKA_QUEUE_DIR: &str = "queue_dir";
|
||||||
|
pub const KAFKA_QUEUE_LIMIT: &str = "queue_limit";
|
||||||
|
pub const KAFKA_TLS_ENABLE: &str = "tls_enable";
|
||||||
|
pub const KAFKA_TLS_CA: &str = "tls_ca";
|
||||||
|
pub const KAFKA_TLS_CLIENT_CERT: &str = "tls_client_cert";
|
||||||
|
pub const KAFKA_TLS_CLIENT_KEY: &str = "tls_client_key";
|
||||||
|
|
||||||
pub const NATS_ADDRESS: &str = "address";
|
pub const NATS_ADDRESS: &str = "address";
|
||||||
pub const NATS_SUBJECT: &str = "subject";
|
pub const NATS_SUBJECT: &str = "subject";
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ pub use constants::env::*;
|
|||||||
#[cfg(feature = "constants")]
|
#[cfg(feature = "constants")]
|
||||||
pub use constants::heal::*;
|
pub use constants::heal::*;
|
||||||
#[cfg(feature = "constants")]
|
#[cfg(feature = "constants")]
|
||||||
|
pub use constants::health::*;
|
||||||
|
#[cfg(feature = "constants")]
|
||||||
|
pub use constants::internode::*;
|
||||||
|
#[cfg(feature = "constants")]
|
||||||
pub use constants::object::*;
|
pub use constants::object::*;
|
||||||
#[cfg(feature = "constants")]
|
#[cfg(feature = "constants")]
|
||||||
pub use constants::profiler::*;
|
pub use constants::profiler::*;
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
/// A list of all valid configuration keys for a Kafka target.
|
||||||
|
pub const NOTIFY_KAFKA_KEYS: &[&str] = &[
|
||||||
|
crate::ENABLE_KEY,
|
||||||
|
crate::KAFKA_BROKERS,
|
||||||
|
crate::KAFKA_TOPIC,
|
||||||
|
crate::KAFKA_ACKS,
|
||||||
|
crate::KAFKA_TLS_ENABLE,
|
||||||
|
crate::KAFKA_TLS_CA,
|
||||||
|
crate::KAFKA_TLS_CLIENT_CERT,
|
||||||
|
crate::KAFKA_TLS_CLIENT_KEY,
|
||||||
|
crate::KAFKA_QUEUE_DIR,
|
||||||
|
crate::KAFKA_QUEUE_LIMIT,
|
||||||
|
crate::COMMENT_KEY,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Kafka Environment Variables
|
||||||
|
pub const ENV_NOTIFY_KAFKA_ENABLE: &str = "RUSTFS_NOTIFY_KAFKA_ENABLE";
|
||||||
|
pub const ENV_NOTIFY_KAFKA_BROKERS: &str = "RUSTFS_NOTIFY_KAFKA_BROKERS";
|
||||||
|
pub const ENV_NOTIFY_KAFKA_TOPIC: &str = "RUSTFS_NOTIFY_KAFKA_TOPIC";
|
||||||
|
pub const ENV_NOTIFY_KAFKA_ACKS: &str = "RUSTFS_NOTIFY_KAFKA_ACKS";
|
||||||
|
pub const ENV_NOTIFY_KAFKA_TLS_ENABLE: &str = "RUSTFS_NOTIFY_KAFKA_TLS_ENABLE";
|
||||||
|
pub const ENV_NOTIFY_KAFKA_TLS_CA: &str = "RUSTFS_NOTIFY_KAFKA_TLS_CA";
|
||||||
|
pub const ENV_NOTIFY_KAFKA_TLS_CLIENT_CERT: &str = "RUSTFS_NOTIFY_KAFKA_TLS_CLIENT_CERT";
|
||||||
|
pub const ENV_NOTIFY_KAFKA_TLS_CLIENT_KEY: &str = "RUSTFS_NOTIFY_KAFKA_TLS_CLIENT_KEY";
|
||||||
|
pub const ENV_NOTIFY_KAFKA_QUEUE_DIR: &str = "RUSTFS_NOTIFY_KAFKA_QUEUE_DIR";
|
||||||
|
pub const ENV_NOTIFY_KAFKA_QUEUE_LIMIT: &str = "RUSTFS_NOTIFY_KAFKA_QUEUE_LIMIT";
|
||||||
|
|
||||||
|
pub const ENV_NOTIFY_KAFKA_KEYS: &[&str; 10] = &[
|
||||||
|
ENV_NOTIFY_KAFKA_ENABLE,
|
||||||
|
ENV_NOTIFY_KAFKA_BROKERS,
|
||||||
|
ENV_NOTIFY_KAFKA_TOPIC,
|
||||||
|
ENV_NOTIFY_KAFKA_ACKS,
|
||||||
|
ENV_NOTIFY_KAFKA_TLS_ENABLE,
|
||||||
|
ENV_NOTIFY_KAFKA_TLS_CA,
|
||||||
|
ENV_NOTIFY_KAFKA_TLS_CLIENT_CERT,
|
||||||
|
ENV_NOTIFY_KAFKA_TLS_CLIENT_KEY,
|
||||||
|
ENV_NOTIFY_KAFKA_QUEUE_DIR,
|
||||||
|
ENV_NOTIFY_KAFKA_QUEUE_LIMIT,
|
||||||
|
];
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
mod arn;
|
mod arn;
|
||||||
|
mod kafka;
|
||||||
mod mqtt;
|
mod mqtt;
|
||||||
mod nats;
|
mod nats;
|
||||||
mod pulsar;
|
mod pulsar;
|
||||||
@@ -20,6 +21,7 @@ mod store;
|
|||||||
mod webhook;
|
mod webhook;
|
||||||
|
|
||||||
pub use arn::*;
|
pub use arn::*;
|
||||||
|
pub use kafka::*;
|
||||||
pub use mqtt::*;
|
pub use mqtt::*;
|
||||||
pub use nats::*;
|
pub use nats::*;
|
||||||
pub use pulsar::*;
|
pub use pulsar::*;
|
||||||
@@ -69,13 +71,13 @@ pub const DEFAULT_NOTIFY_SEND_CONCURRENCY: usize = 64;
|
|||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub const NOTIFY_SUB_SYSTEMS: &[&str] = &[
|
pub const NOTIFY_SUB_SYSTEMS: &[&str] = &[
|
||||||
|
NOTIFY_KAFKA_SUB_SYS,
|
||||||
NOTIFY_MQTT_SUB_SYS,
|
NOTIFY_MQTT_SUB_SYS,
|
||||||
NOTIFY_NATS_SUB_SYS,
|
NOTIFY_NATS_SUB_SYS,
|
||||||
NOTIFY_PULSAR_SUB_SYS,
|
NOTIFY_PULSAR_SUB_SYS,
|
||||||
NOTIFY_WEBHOOK_SUB_SYS,
|
NOTIFY_WEBHOOK_SUB_SYS,
|
||||||
];
|
];
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub const NOTIFY_KAFKA_SUB_SYS: &str = "notify_kafka";
|
pub const NOTIFY_KAFKA_SUB_SYS: &str = "notify_kafka";
|
||||||
pub const NOTIFY_MQTT_SUB_SYS: &str = "notify_mqtt";
|
pub const NOTIFY_MQTT_SUB_SYS: &str = "notify_mqtt";
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ pub const ENV_OBS_ENDPOINT: &str = "RUSTFS_OBS_ENDPOINT";
|
|||||||
pub const ENV_OBS_TRACE_ENDPOINT: &str = "RUSTFS_OBS_TRACE_ENDPOINT";
|
pub const ENV_OBS_TRACE_ENDPOINT: &str = "RUSTFS_OBS_TRACE_ENDPOINT";
|
||||||
pub const ENV_OBS_METRIC_ENDPOINT: &str = "RUSTFS_OBS_METRIC_ENDPOINT";
|
pub const ENV_OBS_METRIC_ENDPOINT: &str = "RUSTFS_OBS_METRIC_ENDPOINT";
|
||||||
pub const ENV_OBS_LOG_ENDPOINT: &str = "RUSTFS_OBS_LOG_ENDPOINT";
|
pub const ENV_OBS_LOG_ENDPOINT: &str = "RUSTFS_OBS_LOG_ENDPOINT";
|
||||||
|
pub const ENV_OBS_ENDPOINT_HEADERS: &str = "RUSTFS_OBS_ENDPOINT_HEADERS";
|
||||||
|
pub const ENV_OBS_ENDPOINT_TRACES_HEADERS: &str = "RUSTFS_OBS_ENDPOINT_TRACES_HEADERS";
|
||||||
|
pub const ENV_OBS_ENDPOINT_METRICS_HEADERS: &str = "RUSTFS_OBS_ENDPOINT_METRICS_HEADERS";
|
||||||
|
pub const ENV_OBS_ENDPOINT_LOGS_HEADERS: &str = "RUSTFS_OBS_ENDPOINT_LOGS_HEADERS";
|
||||||
|
pub const ENV_OBS_ENDPOINT_TIMEOUT_MILLIS: &str = "RUSTFS_OBS_ENDPOINT_TIMEOUT_MILLIS";
|
||||||
|
pub const ENV_OBS_ENDPOINT_TRACES_TIMEOUT_MILLIS: &str = "RUSTFS_OBS_ENDPOINT_TRACES_TIMEOUT_MILLIS";
|
||||||
|
pub const ENV_OBS_ENDPOINT_METRICS_TIMEOUT_MILLIS: &str = "RUSTFS_OBS_ENDPOINT_METRICS_TIMEOUT_MILLIS";
|
||||||
|
pub const ENV_OBS_ENDPOINT_LOGS_TIMEOUT_MILLIS: &str = "RUSTFS_OBS_ENDPOINT_LOGS_TIMEOUT_MILLIS";
|
||||||
pub const ENV_OBS_PROFILING_ENDPOINT: &str = "RUSTFS_OBS_PROFILING_ENDPOINT";
|
pub const ENV_OBS_PROFILING_ENDPOINT: &str = "RUSTFS_OBS_PROFILING_ENDPOINT";
|
||||||
pub const ENV_OBS_USE_STDOUT: &str = "RUSTFS_OBS_USE_STDOUT";
|
pub const ENV_OBS_USE_STDOUT: &str = "RUSTFS_OBS_USE_STDOUT";
|
||||||
pub const ENV_OBS_SAMPLE_RATIO: &str = "RUSTFS_OBS_SAMPLE_RATIO";
|
pub const ENV_OBS_SAMPLE_RATIO: &str = "RUSTFS_OBS_SAMPLE_RATIO";
|
||||||
@@ -108,6 +116,14 @@ mod tests {
|
|||||||
assert_eq!(ENV_OBS_TRACE_ENDPOINT, "RUSTFS_OBS_TRACE_ENDPOINT");
|
assert_eq!(ENV_OBS_TRACE_ENDPOINT, "RUSTFS_OBS_TRACE_ENDPOINT");
|
||||||
assert_eq!(ENV_OBS_METRIC_ENDPOINT, "RUSTFS_OBS_METRIC_ENDPOINT");
|
assert_eq!(ENV_OBS_METRIC_ENDPOINT, "RUSTFS_OBS_METRIC_ENDPOINT");
|
||||||
assert_eq!(ENV_OBS_LOG_ENDPOINT, "RUSTFS_OBS_LOG_ENDPOINT");
|
assert_eq!(ENV_OBS_LOG_ENDPOINT, "RUSTFS_OBS_LOG_ENDPOINT");
|
||||||
|
assert_eq!(ENV_OBS_ENDPOINT_HEADERS, "RUSTFS_OBS_ENDPOINT_HEADERS");
|
||||||
|
assert_eq!(ENV_OBS_ENDPOINT_TRACES_HEADERS, "RUSTFS_OBS_ENDPOINT_TRACES_HEADERS");
|
||||||
|
assert_eq!(ENV_OBS_ENDPOINT_METRICS_HEADERS, "RUSTFS_OBS_ENDPOINT_METRICS_HEADERS");
|
||||||
|
assert_eq!(ENV_OBS_ENDPOINT_LOGS_HEADERS, "RUSTFS_OBS_ENDPOINT_LOGS_HEADERS");
|
||||||
|
assert_eq!(ENV_OBS_ENDPOINT_TIMEOUT_MILLIS, "RUSTFS_OBS_ENDPOINT_TIMEOUT_MILLIS");
|
||||||
|
assert_eq!(ENV_OBS_ENDPOINT_TRACES_TIMEOUT_MILLIS, "RUSTFS_OBS_ENDPOINT_TRACES_TIMEOUT_MILLIS");
|
||||||
|
assert_eq!(ENV_OBS_ENDPOINT_METRICS_TIMEOUT_MILLIS, "RUSTFS_OBS_ENDPOINT_METRICS_TIMEOUT_MILLIS");
|
||||||
|
assert_eq!(ENV_OBS_ENDPOINT_LOGS_TIMEOUT_MILLIS, "RUSTFS_OBS_ENDPOINT_LOGS_TIMEOUT_MILLIS");
|
||||||
assert_eq!(ENV_OBS_PROFILING_ENDPOINT, "RUSTFS_OBS_PROFILING_ENDPOINT");
|
assert_eq!(ENV_OBS_PROFILING_ENDPOINT, "RUSTFS_OBS_PROFILING_ENDPOINT");
|
||||||
assert_eq!(ENV_OBS_USE_STDOUT, "RUSTFS_OBS_USE_STDOUT");
|
assert_eq!(ENV_OBS_USE_STDOUT, "RUSTFS_OBS_USE_STDOUT");
|
||||||
assert_eq!(ENV_OBS_SAMPLE_RATIO, "RUSTFS_OBS_SAMPLE_RATIO");
|
assert_eq!(ENV_OBS_SAMPLE_RATIO, "RUSTFS_OBS_SAMPLE_RATIO");
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ mod tests {
|
|||||||
|
|
||||||
// In production environment, access key and secret key should be different
|
// In production environment, access key and secret key should be different
|
||||||
// These are default values, so being the same is acceptable, but should be warned in documentation
|
// These are default values, so being the same is acceptable, but should be warned in documentation
|
||||||
println!("Warning: Default access key and secret key are the same. Change them in production!");
|
assert_eq!(DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -74,10 +74,8 @@ mod tests {
|
|||||||
// Test security best practices
|
// Test security best practices
|
||||||
|
|
||||||
// These are default values, should be changed in production environments
|
// These are default values, should be changed in production environments
|
||||||
println!("Security Warning: Default credentials detected!");
|
assert_eq!(DEFAULT_ACCESS_KEY, "rustfsadmin");
|
||||||
println!("Access Key: {DEFAULT_ACCESS_KEY}");
|
assert_eq!(DEFAULT_SECRET_KEY, "rustfsadmin");
|
||||||
println!("Secret Key: {DEFAULT_SECRET_KEY}");
|
|
||||||
println!("These should be changed in production environments!");
|
|
||||||
|
|
||||||
// Verify that key lengths meet minimum security requirements
|
// Verify that key lengths meet minimum security requirements
|
||||||
assert!(DEFAULT_ACCESS_KEY.len() >= 8, "Access key should be at least 8 characters");
|
assert!(DEFAULT_ACCESS_KEY.len() >= 8, "Access key should be at least 8 characters");
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ static GLOBAL_ACTIVE_CRED: OnceLock<Credentials> = OnceLock::new();
|
|||||||
/// Global RPC authentication token
|
/// Global RPC authentication token
|
||||||
pub static GLOBAL_RUSTFS_RPC_SECRET: OnceLock<String> = OnceLock::new();
|
pub static GLOBAL_RUSTFS_RPC_SECRET: OnceLock<String> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Public error returned when RPC authentication is not safely configured.
|
||||||
|
pub const RPC_SECRET_REQUIRED_MESSAGE: &str = "RPC authentication secret is not configured";
|
||||||
|
|
||||||
|
/// Operator-facing guidance for configuring RPC authentication safely.
|
||||||
|
pub const RPC_SECRET_REQUIRED_OPERATOR_MESSAGE: &str = "RUSTFS_RPC_SECRET must be set to a non-default value or RUSTFS_SECRET_KEY must be changed from the default for RPC authentication";
|
||||||
|
|
||||||
/// Error type for credentials operations
|
/// Error type for credentials operations
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum CredentialsError {
|
pub enum CredentialsError {
|
||||||
@@ -216,13 +222,39 @@ pub fn gen_secret_key(length: usize) -> std::io::Result<String> {
|
|||||||
/// # Returns
|
/// # Returns
|
||||||
/// * `String` - The RPC authentication token
|
/// * `String` - The RPC authentication token
|
||||||
///
|
///
|
||||||
|
fn resolve_rpc_secret(env_secret: Option<&str>, global_secret: Option<&str>) -> Option<String> {
|
||||||
|
if let Some(secret) = env_secret.map(str::trim).filter(|secret| !secret.is_empty()) {
|
||||||
|
return (secret != DEFAULT_SECRET_KEY).then(|| secret.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
global_secret
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|secret| !secret.is_empty() && *secret != DEFAULT_SECRET_KEY)
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_get_rpc_token() -> std::io::Result<String> {
|
||||||
|
if let Some(secret) = GLOBAL_RUSTFS_RPC_SECRET.get() {
|
||||||
|
return resolve_rpc_secret(None, Some(secret)).ok_or_else(|| Error::other(RPC_SECRET_REQUIRED_MESSAGE));
|
||||||
|
}
|
||||||
|
|
||||||
|
let env_secret = env::var(ENV_RPC_SECRET).ok();
|
||||||
|
let global_secret = get_global_secret_key_opt();
|
||||||
|
let secret = resolve_rpc_secret(env_secret.as_deref(), global_secret.as_deref())
|
||||||
|
.ok_or_else(|| Error::other(RPC_SECRET_REQUIRED_MESSAGE))?;
|
||||||
|
|
||||||
|
match GLOBAL_RUSTFS_RPC_SECRET.set(secret.clone()) {
|
||||||
|
Ok(()) => Ok(secret),
|
||||||
|
Err(_) => GLOBAL_RUSTFS_RPC_SECRET
|
||||||
|
.get()
|
||||||
|
.and_then(|stored| resolve_rpc_secret(None, Some(stored)))
|
||||||
|
.ok_or_else(|| Error::other(RPC_SECRET_REQUIRED_MESSAGE)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[deprecated(note = "use try_get_rpc_token to handle missing RPC secrets explicitly")]
|
||||||
pub fn get_rpc_token() -> String {
|
pub fn get_rpc_token() -> String {
|
||||||
GLOBAL_RUSTFS_RPC_SECRET
|
try_get_rpc_token().expect(RPC_SECRET_REQUIRED_MESSAGE)
|
||||||
.get_or_init(|| {
|
|
||||||
env::var(ENV_RPC_SECRET)
|
|
||||||
.unwrap_or_else(|_| get_global_secret_key_opt().unwrap_or_else(|| DEFAULT_SECRET_KEY.to_string()))
|
|
||||||
})
|
|
||||||
.clone()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A wrapper struct for masking sensitive strings in Debug implementations.
|
/// A wrapper struct for masking sensitive strings in Debug implementations.
|
||||||
@@ -301,7 +333,7 @@ impl fmt::Debug for Credentials {
|
|||||||
f.debug_struct("Credentials")
|
f.debug_struct("Credentials")
|
||||||
.field("access_key", &self.access_key)
|
.field("access_key", &self.access_key)
|
||||||
.field("secret_key", &Masked(Some(&self.secret_key)))
|
.field("secret_key", &Masked(Some(&self.secret_key)))
|
||||||
.field("session_token", &self.session_token)
|
.field("session_token", &Masked(Some(&self.session_token)))
|
||||||
.field("expiration", &self.expiration)
|
.field("expiration", &self.expiration)
|
||||||
.field("status", &self.status)
|
.field("status", &self.status)
|
||||||
.field("parent_user", &self.parent_user)
|
.field("parent_user", &self.parent_user)
|
||||||
@@ -495,6 +527,63 @@ mod tests {
|
|||||||
assert!(!key.contains('='));
|
assert!(!key.contains('='));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_rpc_secret_rejects_default_fallback() {
|
||||||
|
assert!(resolve_rpc_secret(None, None).is_none());
|
||||||
|
assert!(resolve_rpc_secret(None, Some(DEFAULT_SECRET_KEY)).is_none());
|
||||||
|
assert!(resolve_rpc_secret(Some(DEFAULT_SECRET_KEY), Some("custom-global-secret")).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rpc_secret_public_error_omits_configuration_details() {
|
||||||
|
assert!(!RPC_SECRET_REQUIRED_MESSAGE.contains("RUSTFS_"));
|
||||||
|
assert!(!RPC_SECRET_REQUIRED_MESSAGE.contains(DEFAULT_SECRET_KEY));
|
||||||
|
assert!(RPC_SECRET_REQUIRED_OPERATOR_MESSAGE.contains("RUSTFS_RPC_SECRET"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
|
#[test]
|
||||||
|
fn test_get_rpc_token_preserves_string_return_type() {
|
||||||
|
fn assert_string_return(_: fn() -> String) {}
|
||||||
|
|
||||||
|
assert_string_return(get_rpc_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_rpc_secret_accepts_non_default_secret() {
|
||||||
|
assert_eq!(resolve_rpc_secret(Some("custom-rpc-secret"), None).as_deref(), Some("custom-rpc-secret"));
|
||||||
|
assert_eq!(
|
||||||
|
resolve_rpc_secret(None, Some("custom-global-secret")).as_deref(),
|
||||||
|
Some("custom-global-secret")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_rpc_secret_trims_and_falls_back_from_blank_env() {
|
||||||
|
assert_eq!(
|
||||||
|
resolve_rpc_secret(Some(" custom-rpc-secret "), None).as_deref(),
|
||||||
|
Some("custom-rpc-secret")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_rpc_secret(Some(""), Some("custom-global-secret")).as_deref(),
|
||||||
|
Some("custom-global-secret")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_rpc_secret(Some(" "), Some(" custom-global-secret ")).as_deref(),
|
||||||
|
Some("custom-global-secret")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve_rpc_secret(Some(" "), Some("custom-global-secret")).as_deref(),
|
||||||
|
Some("custom-global-secret")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_resolve_rpc_secret_returns_none_for_trimmed_default_secret() {
|
||||||
|
let padded_default_secret = format!(" {} ", DEFAULT_SECRET_KEY);
|
||||||
|
assert!(resolve_rpc_secret(Some(padded_default_secret.as_str()), Some("custom-global-secret")).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_masked_debug() {
|
fn test_masked_debug() {
|
||||||
// Test None
|
// Test None
|
||||||
@@ -524,6 +613,24 @@ mod tests {
|
|||||||
assert_eq!(format!("{:?}", Masked(Some("中文测试"))), "中***试|4");
|
assert_eq!(format!("{:?}", Masked(Some("中文测试"))), "中***试|4");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_credentials_debug_masks_sensitive_fields() {
|
||||||
|
let cred = Credentials {
|
||||||
|
access_key: "debug-access-key".to_string(),
|
||||||
|
secret_key: "debug-secret-key".to_string(),
|
||||||
|
session_token: "debug-session-token".to_string(),
|
||||||
|
parent_user: "parent-user".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let output = format!("{cred:?}");
|
||||||
|
|
||||||
|
assert!(output.contains("debug-access-key"));
|
||||||
|
assert!(output.contains("parent-user"));
|
||||||
|
assert!(!output.contains("debug-secret-key"));
|
||||||
|
assert!(!output.contains("debug-session-token"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_credentials_expiration_serialize_as_rfc3339() {
|
fn test_credentials_expiration_serialize_as_rfc3339() {
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|||||||
@@ -73,3 +73,4 @@ rcgen.workspace = true
|
|||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
rustls.workspace = true
|
rustls.workspace = true
|
||||||
zip.workspace = true
|
zip.workspace = true
|
||||||
|
clap.workspace = true
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use e2e_test::tls_gen::{Args, run};
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
let out_dir = run(Args::parse())?;
|
||||||
|
println!("Generated RustFS TLS bundle in {}", out_dir.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! CopyObject metadata replacement regression tests.
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::common::{RustFSTestEnvironment, init_logging};
|
||||||
|
use aws_sdk_s3::primitives::ByteStream;
|
||||||
|
use aws_sdk_s3::types::MetadataDirective;
|
||||||
|
use serial_test::serial;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_self_copy_replace_metadata_preserves_readable_object() {
|
||||||
|
init_logging();
|
||||||
|
info!("Issue #2789: self-copy metadata replacement must preserve object data");
|
||||||
|
|
||||||
|
let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
|
||||||
|
env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS");
|
||||||
|
|
||||||
|
let client = env.create_s3_client();
|
||||||
|
let bucket = "self-copy-metadata-replace-test";
|
||||||
|
let key = "assets/chunk-2F3R7JUG.js";
|
||||||
|
let content = b"console.log('metadata replacement should keep object data readable');";
|
||||||
|
|
||||||
|
client
|
||||||
|
.create_bucket()
|
||||||
|
.bucket(bucket)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to create bucket");
|
||||||
|
|
||||||
|
client
|
||||||
|
.put_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.content_type("text/javascript; charset=utf-8")
|
||||||
|
.metadata("mtime", "1777992333")
|
||||||
|
.metadata("stale", "must-be-removed")
|
||||||
|
.body(ByteStream::from_static(content))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("PUT failed");
|
||||||
|
|
||||||
|
client
|
||||||
|
.copy_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.copy_source(format!("{bucket}/{key}"))
|
||||||
|
.metadata_directive(MetadataDirective::Replace)
|
||||||
|
.content_type("text/javascript; charset=utf-8")
|
||||||
|
.metadata("mtime", "1777992348")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("self CopyObject with metadata replacement failed");
|
||||||
|
|
||||||
|
let head_resp = client
|
||||||
|
.head_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("HEAD failed after self-copy");
|
||||||
|
assert_eq!(head_resp.content_length(), Some(content.len() as i64));
|
||||||
|
assert_eq!(
|
||||||
|
head_resp.metadata().and_then(|metadata| metadata.get("mtime")),
|
||||||
|
Some(&"1777992348".to_string()),
|
||||||
|
"HEAD should return replaced metadata"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
head_resp.metadata().and_then(|metadata| metadata.get("stale")),
|
||||||
|
None,
|
||||||
|
"HEAD should not return metadata omitted by REPLACE"
|
||||||
|
);
|
||||||
|
|
||||||
|
let get_resp = client
|
||||||
|
.get_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("GET failed after self-copy");
|
||||||
|
let body = get_resp
|
||||||
|
.body
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.expect("Failed to collect GET body")
|
||||||
|
.into_bytes();
|
||||||
|
assert_eq!(body.as_ref(), content, "self-copy metadata replacement must not drop object data");
|
||||||
|
|
||||||
|
client
|
||||||
|
.copy_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.copy_source(format!("{bucket}/{key}"))
|
||||||
|
.metadata_directive(MetadataDirective::Replace)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("self CopyObject with empty metadata replacement failed");
|
||||||
|
|
||||||
|
let empty_head_resp = client
|
||||||
|
.head_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("HEAD failed after empty metadata replacement");
|
||||||
|
assert_eq!(
|
||||||
|
empty_head_resp.metadata().and_then(|metadata| metadata.get("mtime")),
|
||||||
|
None,
|
||||||
|
"HEAD should not return metadata omitted by empty REPLACE"
|
||||||
|
);
|
||||||
|
|
||||||
|
let empty_get_resp = client
|
||||||
|
.get_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("GET failed after empty metadata replacement");
|
||||||
|
let empty_body = empty_get_resp
|
||||||
|
.body
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.expect("Failed to collect GET body after empty metadata replacement")
|
||||||
|
.into_bytes();
|
||||||
|
assert_eq!(empty_body.as_ref(), content, "empty metadata replacement must not drop object data");
|
||||||
|
|
||||||
|
env.stop_server();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ use crate::common::{
|
|||||||
};
|
};
|
||||||
use aws_sdk_s3::config::{Credentials, Region};
|
use aws_sdk_s3::config::{Credentials, Region};
|
||||||
use aws_sdk_s3::primitives::ByteStream;
|
use aws_sdk_s3::primitives::ByteStream;
|
||||||
use aws_sdk_s3::types::{Tag, Tagging};
|
use aws_sdk_s3::types::{Delete, ObjectIdentifier, Tag, Tagging};
|
||||||
use aws_sdk_s3::{Client, Config};
|
use aws_sdk_s3::{Client, Config};
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
@@ -318,11 +318,18 @@ async fn test_e2e_sts_assume_role_session_policy_existing_object_tag() -> Result
|
|||||||
|
|
||||||
let rw = serde_json::to_string(&serde_json::json!({
|
let rw = serde_json::to_string(&serde_json::json!({
|
||||||
"Version": "2012-10-17",
|
"Version": "2012-10-17",
|
||||||
"Statement": [{
|
"Statement": [
|
||||||
"Effect": "Allow",
|
{
|
||||||
"Action": ["s3:*"],
|
"Effect": "Allow",
|
||||||
"Resource": ["arn:aws:s3:::*"]
|
"Action": ["s3:*"],
|
||||||
}]
|
"Resource": ["arn:aws:s3:::*"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": ["sts:AssumeRole"],
|
||||||
|
"Resource": ["arn:aws:s3:::*"]
|
||||||
|
}
|
||||||
|
]
|
||||||
}))?;
|
}))?;
|
||||||
admin_add_canned_policy(&env, &policy_readwrite, &rw).await?;
|
admin_add_canned_policy(&env, &policy_readwrite, &rw).await?;
|
||||||
admin_attach_policy_to_user(&env, &policy_readwrite, &parent).await?;
|
admin_attach_policy_to_user(&env, &policy_readwrite, &parent).await?;
|
||||||
@@ -362,3 +369,114 @@ async fn test_e2e_sts_assume_role_session_policy_existing_object_tag() -> Result
|
|||||||
info!("test_e2e_sts_assume_role_session_policy_existing_object_tag passed");
|
info!("test_e2e_sts_assume_role_session_policy_existing_object_tag passed");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// STS inline session policy: DeleteObjects must evaluate `s3:DeleteObject` per requested object key.
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_e2e_sts_session_policy_delete_objects_object_prefix_only() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
init_logging();
|
||||||
|
if !awscurl_available() {
|
||||||
|
info!("Skipping test_e2e_sts_session_policy_delete_objects_object_prefix_only: awscurl not available");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let suffix = Uuid::new_v4();
|
||||||
|
let parent = format!("e2e-sts-del-par-{suffix}");
|
||||||
|
let parent_secret = "longSecretKeyForParentDelete99!";
|
||||||
|
let policy_readwrite = format!("e2e-sts-del-rw-{suffix}");
|
||||||
|
let bucket = format!("e2e-sts-del-bkt-{suffix}");
|
||||||
|
let allowed_key = "allowed/table/data.parquet";
|
||||||
|
let denied_key = "denied/table/data.parquet";
|
||||||
|
|
||||||
|
let mut env = RustFSTestEnvironment::new().await?;
|
||||||
|
env.start_rustfs_server(vec![]).await?;
|
||||||
|
|
||||||
|
let admin = env.create_s3_client();
|
||||||
|
admin_create_user(&env, &parent, parent_secret).await?;
|
||||||
|
|
||||||
|
let rw = serde_json::to_string(&serde_json::json!({
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": ["s3:*"],
|
||||||
|
"Resource": ["arn:aws:s3:::*"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": ["sts:AssumeRole"],
|
||||||
|
"Resource": ["arn:aws:s3:::*"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}))?;
|
||||||
|
admin_add_canned_policy(&env, &policy_readwrite, &rw).await?;
|
||||||
|
admin_attach_policy_to_user(&env, &policy_readwrite, &parent).await?;
|
||||||
|
|
||||||
|
let parent_client = user_client(&env, &parent, parent_secret);
|
||||||
|
parent_client.create_bucket().bucket(&bucket).send().await?;
|
||||||
|
parent_client
|
||||||
|
.put_object()
|
||||||
|
.bucket(&bucket)
|
||||||
|
.key(allowed_key)
|
||||||
|
.body(ByteStream::from_static(b"allowed-delete-data"))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
parent_client
|
||||||
|
.put_object()
|
||||||
|
.bucket(&bucket)
|
||||||
|
.key(denied_key)
|
||||||
|
.body(ByteStream::from_static(b"denied-delete-data"))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let session_policy = serde_json::json!({
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": ["s3:DeleteObject"],
|
||||||
|
"Resource": [format!("arn:aws:s3:::{}/allowed/*", bucket)]
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let (ak, sk, token) = assume_role_with_session_policy(&env, &parent, parent_secret, &session_policy).await?;
|
||||||
|
let session_client = sts_session_client(&env, &ak, &sk, &token);
|
||||||
|
|
||||||
|
let delete = Delete::builder()
|
||||||
|
.objects(ObjectIdentifier::builder().key(allowed_key).build()?)
|
||||||
|
.objects(ObjectIdentifier::builder().key(denied_key).build()?)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let result = session_client.delete_objects().bucket(&bucket).delete(delete).send().await?;
|
||||||
|
|
||||||
|
assert_eq!(result.deleted().len(), 1, "only the allowed-prefix object should be deleted");
|
||||||
|
assert!(
|
||||||
|
result.deleted().iter().any(|deleted| deleted.key() == Some(allowed_key)),
|
||||||
|
"DeleteObjects response should report the allowed-prefix object as deleted"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(result.errors().len(), 1, "the out-of-prefix object should return one per-key error");
|
||||||
|
let error = &result.errors()[0];
|
||||||
|
assert_eq!(error.key(), Some(denied_key));
|
||||||
|
assert_eq!(error.code(), Some("AccessDenied"));
|
||||||
|
|
||||||
|
let allowed_head = parent_client.head_object().bucket(&bucket).key(allowed_key).send().await;
|
||||||
|
assert!(allowed_head.is_err(), "allowed-prefix object should have been deleted");
|
||||||
|
|
||||||
|
parent_client
|
||||||
|
.head_object()
|
||||||
|
.bucket(&bucket)
|
||||||
|
.key(denied_key)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("out-of-prefix object should remain after per-key AccessDenied");
|
||||||
|
|
||||||
|
let _ = admin.delete_object().bucket(&bucket).key(allowed_key).send().await;
|
||||||
|
let _ = admin.delete_object().bucket(&bucket).key(denied_key).send().await;
|
||||||
|
let _ = admin.delete_bucket().bucket(&bucket).send().await;
|
||||||
|
admin_remove_user(&env, &parent).await;
|
||||||
|
admin_remove_policy(&env, &policy_readwrite).await;
|
||||||
|
|
||||||
|
info!("test_e2e_sts_session_policy_delete_objects_object_prefix_only passed");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
use crate::common::{RustFSTestEnvironment, init_logging};
|
||||||
|
use aws_sdk_s3::primitives::ByteStream;
|
||||||
|
use serial_test::serial;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
const RANGE_HEAD_BUCKET: &str = "range-head-test-bucket";
|
||||||
|
const RANGE_HEAD_KEY: &str = "range-head-object.bin";
|
||||||
|
const ACCEPT_RANGES_BYTES: &str = "bytes";
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn head_object_advertises_accept_ranges() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
init_logging();
|
||||||
|
info!("Starting HeadObject Accept-Ranges regression test");
|
||||||
|
|
||||||
|
let mut env = RustFSTestEnvironment::new().await?;
|
||||||
|
env.start_rustfs_server(Vec::new()).await?;
|
||||||
|
|
||||||
|
let client = env.create_s3_client();
|
||||||
|
env.create_test_bucket(RANGE_HEAD_BUCKET).await?;
|
||||||
|
|
||||||
|
client
|
||||||
|
.put_object()
|
||||||
|
.bucket(RANGE_HEAD_BUCKET)
|
||||||
|
.key(RANGE_HEAD_KEY)
|
||||||
|
.body(ByteStream::from_static(b"0123456789abcdef"))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let head = client
|
||||||
|
.head_object()
|
||||||
|
.bucket(RANGE_HEAD_BUCKET)
|
||||||
|
.key(RANGE_HEAD_KEY)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
assert_eq!(
|
||||||
|
head.accept_ranges(),
|
||||||
|
Some(ACCEPT_RANGES_BYTES),
|
||||||
|
"HeadObject should advertise byte range support"
|
||||||
|
);
|
||||||
|
|
||||||
|
client
|
||||||
|
.delete_object()
|
||||||
|
.bucket(RANGE_HEAD_BUCKET)
|
||||||
|
.key(RANGE_HEAD_KEY)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
env.delete_test_bucket(RANGE_HEAD_BUCKET).await?;
|
||||||
|
env.stop_server();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -104,6 +104,12 @@ mod checksum_upload_test;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod group_delete_test;
|
mod group_delete_test;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod head_object_range_test;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod copy_object_metadata_test;
|
||||||
|
|
||||||
// S3 dummy-compat bucket API tests
|
// S3 dummy-compat bucket API tests
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod bucket_logging_test;
|
mod bucket_logging_test;
|
||||||
@@ -125,3 +131,5 @@ mod replication_extension_test;
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod snowball_auto_extract_test;
|
mod snowball_auto_extract_test;
|
||||||
|
|
||||||
|
pub mod tls_gen;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
mod tests {
|
mod tests {
|
||||||
use crate::common::{RustFSTestEnvironment, init_logging};
|
use crate::common::{RustFSTestEnvironment, init_logging};
|
||||||
use aws_sdk_s3::Client;
|
use aws_sdk_s3::Client;
|
||||||
|
use aws_sdk_s3::primitives::ByteStream;
|
||||||
use aws_sdk_s3::types::{BucketVersioningStatus, VersioningConfiguration};
|
use aws_sdk_s3::types::{BucketVersioningStatus, VersioningConfiguration};
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
@@ -179,4 +180,84 @@ mod tests {
|
|||||||
"Delete marker should no longer be latest after the second put"
|
"Delete marker should no longer be latest after the second put"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_list_object_versions_prefix_with_marker_object_returns_children() {
|
||||||
|
init_logging();
|
||||||
|
info!("🧪 TEST: ListObjectVersions returns prefix children when a marker object also exists");
|
||||||
|
|
||||||
|
let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
|
||||||
|
env.start_rustfs_server(vec![]).await.expect("Failed to start RustFS");
|
||||||
|
|
||||||
|
let client = create_s3_client(&env);
|
||||||
|
let bucket = "test-list-versions-prefix-marker";
|
||||||
|
let marker_key = "data01";
|
||||||
|
let child_keys = [
|
||||||
|
"data01/meta/dump-2026-04-08-053205.json.gz",
|
||||||
|
"data01/meta/dump-2026-04-08-063209.json.gz",
|
||||||
|
];
|
||||||
|
|
||||||
|
client
|
||||||
|
.create_bucket()
|
||||||
|
.bucket(bucket)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to create bucket");
|
||||||
|
|
||||||
|
client
|
||||||
|
.put_bucket_versioning()
|
||||||
|
.bucket(bucket)
|
||||||
|
.versioning_configuration(
|
||||||
|
VersioningConfiguration::builder()
|
||||||
|
.status(BucketVersioningStatus::Suspended)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to suspend versioning");
|
||||||
|
|
||||||
|
client
|
||||||
|
.put_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(marker_key)
|
||||||
|
.body(ByteStream::from_static(b""))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to put marker object");
|
||||||
|
|
||||||
|
for key in child_keys {
|
||||||
|
client
|
||||||
|
.put_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.body(ByteStream::from_static(b"payload"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to put child object");
|
||||||
|
}
|
||||||
|
|
||||||
|
let listing = client
|
||||||
|
.list_object_versions()
|
||||||
|
.bucket(bucket)
|
||||||
|
.prefix("data01/")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("Failed to list object versions by prefix");
|
||||||
|
|
||||||
|
let version_keys: Vec<_> = listing.versions().iter().filter_map(|version| version.key()).collect();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
version_keys.len(),
|
||||||
|
child_keys.len(),
|
||||||
|
"ListObjectVersions with a trailing slash prefix should include child objects even when the marker object exists"
|
||||||
|
);
|
||||||
|
|
||||||
|
for key in child_keys {
|
||||||
|
assert!(
|
||||||
|
version_keys.contains(&key),
|
||||||
|
"ListObjectVersions(prefix=data01/) should include child object {key}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,11 +120,6 @@ impl LockClient for GrpcLockClient {
|
|||||||
.map_err(|e| LockError::internal(e.to_string()))?
|
.map_err(|e| LockError::internal(e.to_string()))?
|
||||||
.into_inner();
|
.into_inner();
|
||||||
|
|
||||||
// Check for explicit error first
|
|
||||||
if let Some(error_info) = resp.error_info {
|
|
||||||
return Err(LockError::internal(error_info));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the lock acquisition was successful
|
// Check if the lock acquisition was successful
|
||||||
if resp.success {
|
if resp.success {
|
||||||
Ok(LockResponse::success(
|
Ok(LockResponse::success(
|
||||||
@@ -134,7 +129,8 @@ impl LockClient for GrpcLockClient {
|
|||||||
} else {
|
} else {
|
||||||
// Lock acquisition failed
|
// Lock acquisition failed
|
||||||
Ok(LockResponse::failure(
|
Ok(LockResponse::failure(
|
||||||
"Lock acquisition failed on remote server".to_string(),
|
resp.error_info
|
||||||
|
.unwrap_or_else(|| "Lock acquisition failed on remote server".to_string()),
|
||||||
std::time::Duration::ZERO,
|
std::time::Duration::ZERO,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,18 @@ fn lock_result_from_error(error: impl Into<String>) -> GenerallyLockResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lock_result_from_release(lock_id: &rustfs_lock::LockId, success: bool) -> GenerallyLockResult {
|
||||||
|
if success {
|
||||||
|
GenerallyLockResult {
|
||||||
|
success: true,
|
||||||
|
error_info: None,
|
||||||
|
lock_info: None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lock_result_from_error(format!("lock not found for release: {lock_id}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Minimal NodeService implementation that only supports Lock RPCs
|
/// Minimal NodeService implementation that only supports Lock RPCs
|
||||||
/// Used for testing distributed lock scenarios with real gRPC
|
/// Used for testing distributed lock scenarios with real gRPC
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -102,7 +114,7 @@ impl NodeService for MinimalLockNodeService {
|
|||||||
let lock_info_json = result.lock_info.as_ref().and_then(|info| serde_json::to_string(info).ok());
|
let lock_info_json = result.lock_info.as_ref().and_then(|info| serde_json::to_string(info).ok());
|
||||||
Ok(Response::new(GenerallyLockResponse {
|
Ok(Response::new(GenerallyLockResponse {
|
||||||
success: result.success,
|
success: result.success,
|
||||||
error_info: None,
|
error_info: result.error,
|
||||||
lock_info: lock_info_json,
|
lock_info: lock_info_json,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -131,11 +143,14 @@ impl NodeService for MinimalLockNodeService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match self.lock_client.release(&args.lock_id).await {
|
match self.lock_client.release(&args.lock_id).await {
|
||||||
Ok(success) => Ok(Response::new(GenerallyLockResponse {
|
Ok(success) => {
|
||||||
success,
|
let result = lock_result_from_release(&args.lock_id, success);
|
||||||
error_info: None,
|
Ok(Response::new(GenerallyLockResponse {
|
||||||
lock_info: None,
|
success: result.success,
|
||||||
})),
|
error_info: result.error_info,
|
||||||
|
lock_info: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
Err(err) => Ok(Response::new(GenerallyLockResponse {
|
Err(err) => Ok(Response::new(GenerallyLockResponse {
|
||||||
success: false,
|
success: false,
|
||||||
error_info: Some(format!(
|
error_info: Some(format!(
|
||||||
@@ -161,11 +176,14 @@ impl NodeService for MinimalLockNodeService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match self.lock_client.force_release(&args.lock_id).await {
|
match self.lock_client.force_release(&args.lock_id).await {
|
||||||
Ok(success) => Ok(Response::new(GenerallyLockResponse {
|
Ok(success) => {
|
||||||
success,
|
let result = lock_result_from_release(&args.lock_id, success);
|
||||||
error_info: None,
|
Ok(Response::new(GenerallyLockResponse {
|
||||||
lock_info: None,
|
success: result.success,
|
||||||
})),
|
error_info: result.error_info,
|
||||||
|
lock_info: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
Err(err) => Ok(Response::new(GenerallyLockResponse {
|
Err(err) => Ok(Response::new(GenerallyLockResponse {
|
||||||
success: false,
|
success: false,
|
||||||
error_info: Some(format!(
|
error_info: Some(format!(
|
||||||
@@ -271,10 +289,9 @@ impl NodeService for MinimalLockNodeService {
|
|||||||
Ok(batch_results) => {
|
Ok(batch_results) => {
|
||||||
for (result_idx, success) in batch_results.into_iter().enumerate() {
|
for (result_idx, success) in batch_results.into_iter().enumerate() {
|
||||||
if let Some(request_idx) = valid_indices.get(result_idx) {
|
if let Some(request_idx) = valid_indices.get(result_idx) {
|
||||||
results[*request_idx] = GenerallyLockResult {
|
results[*request_idx] = match lock_ids.get(result_idx) {
|
||||||
success,
|
Some(lock_id) => lock_result_from_release(lock_id, success),
|
||||||
error_info: None,
|
None => lock_result_from_error(format!("unlock response index out of range: {result_idx}")),
|
||||||
lock_info: None,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
//! Regression test for TLS/HTTP2 `HEAD` responses on missing objects.
|
||||||
|
//!
|
||||||
|
//! Before the fix, RustFS returned `404` for a missing object but still wrote
|
||||||
|
//! the XML error payload on a `HEAD` request. Under HTTP/2 this emitted DATA
|
||||||
|
//! frames after the response headers, which clients surfaced as a protocol
|
||||||
|
//! error. This test keeps the request at the raw HTTPS layer so it can validate
|
||||||
|
//! the final wire-facing behavior rather than SDK-level error mapping.
|
||||||
|
|
||||||
|
#![cfg(test)]
|
||||||
|
|
||||||
|
use crate::common::{RustFSTestEnvironment, init_logging, rustfs_binary_path};
|
||||||
|
use http::Version;
|
||||||
|
use http::header::HOST;
|
||||||
|
use rcgen::generate_simple_self_signed;
|
||||||
|
use reqwest::{Certificate, Client, Response, StatusCode};
|
||||||
|
use rustfs_signer::constants::UNSIGNED_PAYLOAD;
|
||||||
|
use rustfs_signer::sign_v4;
|
||||||
|
use s3s::Body;
|
||||||
|
use serial_test::serial;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
use tokio::fs;
|
||||||
|
use tokio::time::{Duration, sleep};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
const ACCESS_KEY: &str = "rustfsadmin";
|
||||||
|
const SECRET_KEY: &str = "rustfsadmin";
|
||||||
|
const BUCKET: &str = "test-head-tls-bodyless-bucket";
|
||||||
|
|
||||||
|
async fn generate_tls_bundle(tls_dir: &Path) -> Result<Vec<u8>, Box<dyn Error + Send + Sync>> {
|
||||||
|
fs::create_dir_all(tls_dir).await?;
|
||||||
|
let cert = generate_simple_self_signed(vec!["localhost".to_string(), "127.0.0.1".to_string()])?;
|
||||||
|
let cert_pem = cert.cert.pem();
|
||||||
|
let key_pem = cert.signing_key.serialize_pem();
|
||||||
|
|
||||||
|
fs::write(tls_dir.join("rustfs_cert.pem"), cert_pem.as_bytes()).await?;
|
||||||
|
fs::write(tls_dir.join("rustfs_key.pem"), key_pem.as_bytes()).await?;
|
||||||
|
|
||||||
|
Ok(cert_pem.into_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_https_h2_client(ca_pem: &[u8]) -> Result<Client, Box<dyn Error + Send + Sync>> {
|
||||||
|
let _ca_cert = Certificate::from_pem(ca_pem)?;
|
||||||
|
Ok(Client::builder()
|
||||||
|
.no_proxy()
|
||||||
|
.no_gzip()
|
||||||
|
.no_brotli()
|
||||||
|
.no_zstd()
|
||||||
|
.no_deflate()
|
||||||
|
.danger_accept_invalid_certs(true)
|
||||||
|
.build()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn signed_empty_request(
|
||||||
|
client: &Client,
|
||||||
|
method: http::Method,
|
||||||
|
url: &str,
|
||||||
|
) -> Result<Response, Box<dyn Error + Send + Sync>> {
|
||||||
|
let uri = url.parse::<http::Uri>()?;
|
||||||
|
let authority = uri.authority().ok_or("request URL missing authority")?.to_string();
|
||||||
|
let request = http::Request::builder()
|
||||||
|
.method(method.as_str())
|
||||||
|
.uri(uri)
|
||||||
|
.header(HOST, authority)
|
||||||
|
.header("x-amz-content-sha256", UNSIGNED_PAYLOAD)
|
||||||
|
.body(Body::empty())?;
|
||||||
|
|
||||||
|
let signed = sign_v4(request, 0, ACCESS_KEY, SECRET_KEY, "", "us-east-1");
|
||||||
|
|
||||||
|
let reqwest_method = reqwest::Method::from_bytes(method.as_str().as_bytes())?;
|
||||||
|
let mut builder = client.request(reqwest_method, url);
|
||||||
|
for (name, value) in signed.headers() {
|
||||||
|
builder = builder.header(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(builder.send().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ensure_bucket_exists(client: &Client, endpoint: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
|
let bucket_url = format!("{endpoint}/{BUCKET}/");
|
||||||
|
let response = signed_empty_request(client, http::Method::HEAD, &bucket_url).await?;
|
||||||
|
|
||||||
|
if response.status() == StatusCode::OK {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = signed_empty_request(client, http::Method::PUT, &bucket_url).await?;
|
||||||
|
match response.status() {
|
||||||
|
StatusCode::OK => Ok(()),
|
||||||
|
StatusCode::CONFLICT => Ok(()),
|
||||||
|
status => Err(format!("unexpected bucket setup status: {status}").into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_tls_server_ready(client: &Client, endpoint: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
|
let ready_url = format!("{endpoint}/");
|
||||||
|
for _attempt in 0..60 {
|
||||||
|
match signed_empty_request(client, http::Method::GET, &ready_url).await {
|
||||||
|
Ok(response) if response.status().is_success() => return Ok(()),
|
||||||
|
Ok(_) | Err(_) => sleep(Duration::from_millis(500)).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err("RustFS TLS server failed to become ready within 30 seconds".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_tls_rustfs_server(env: &mut RustFSTestEnvironment, tls_dir: &Path) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
|
let binary_path = rustfs_binary_path();
|
||||||
|
let mut command = Command::new(&binary_path);
|
||||||
|
command
|
||||||
|
.env("RUST_LOG", "rustfs=info,rustfs_notify=debug")
|
||||||
|
.env("RUSTFS_TLS_PATH", tls_dir)
|
||||||
|
.current_dir(&env.temp_dir);
|
||||||
|
|
||||||
|
for key in [
|
||||||
|
"RUSTFS_ADDRESS",
|
||||||
|
"RUSTFS_VOLUMES",
|
||||||
|
"RUSTFS_ACCESS_KEY",
|
||||||
|
"RUSTFS_SECRET_KEY",
|
||||||
|
"RUSTFS_TLS_PATH",
|
||||||
|
"RUSTFS_OBS_LOG_DIRECTORY",
|
||||||
|
] {
|
||||||
|
command.env_remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
let process = command
|
||||||
|
.env("RUSTFS_TLS_PATH", tls_dir)
|
||||||
|
.env("RUSTFS_CONSOLE_ENABLE", "false")
|
||||||
|
.args([
|
||||||
|
"--address",
|
||||||
|
&env.address,
|
||||||
|
"--access-key",
|
||||||
|
&env.access_key,
|
||||||
|
"--secret-key",
|
||||||
|
&env.secret_key,
|
||||||
|
&env.temp_dir,
|
||||||
|
])
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
env.process = Some(process);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_head_missing_object_over_tls_http2_is_bodyless() -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
|
init_logging();
|
||||||
|
|
||||||
|
let mut env = RustFSTestEnvironment::new().await?;
|
||||||
|
let tls_dir = std::path::PathBuf::from(&env.temp_dir).join("tls");
|
||||||
|
let ca_pem = generate_tls_bundle(&tls_dir).await?;
|
||||||
|
start_tls_rustfs_server(&mut env, &tls_dir).await?;
|
||||||
|
|
||||||
|
let endpoint = format!("https://{}", env.address);
|
||||||
|
let client = local_https_h2_client(&ca_pem)?;
|
||||||
|
wait_for_tls_server_ready(&client, &endpoint).await?;
|
||||||
|
ensure_bucket_exists(&client, &endpoint).await?;
|
||||||
|
|
||||||
|
let missing_key = "head-does-not-exist.txt";
|
||||||
|
let object_url = format!("{endpoint}/{BUCKET}/{missing_key}");
|
||||||
|
|
||||||
|
let get_response = signed_empty_request(&client, http::Method::GET, &object_url).await?;
|
||||||
|
assert_eq!(get_response.status(), StatusCode::NOT_FOUND);
|
||||||
|
let get_version = get_response.version();
|
||||||
|
let get_body = get_response.bytes().await?;
|
||||||
|
let get_body_text = String::from_utf8_lossy(&get_body);
|
||||||
|
assert!(
|
||||||
|
get_body_text.contains("<Code>NoSuchKey</Code>") || get_body_text.contains("<Code>NoSuchObject</Code>"),
|
||||||
|
"GET missing-object error body should expose NoSuchKey/NoSuchObject, got: {}",
|
||||||
|
get_body_text
|
||||||
|
);
|
||||||
|
info!("GET missing object over TLS used {:?} and returned {} bytes", get_version, get_body.len());
|
||||||
|
|
||||||
|
let head_response = signed_empty_request(&client, http::Method::HEAD, &object_url).await?;
|
||||||
|
assert_eq!(head_response.status(), StatusCode::NOT_FOUND);
|
||||||
|
assert_eq!(head_response.version(), Version::HTTP_2, "HEAD regression test must exercise HTTP/2");
|
||||||
|
let head_body = head_response.bytes().await?;
|
||||||
|
assert!(
|
||||||
|
head_body.is_empty(),
|
||||||
|
"HEAD missing-object response must not send body bytes over TLS/HTTP2, got {} bytes: {:?}",
|
||||||
|
head_body.len(),
|
||||||
|
head_body
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -275,6 +275,41 @@ async fn test_grpc_lock_client_batch_acquire_and_release() {
|
|||||||
handle.abort();
|
handle.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_grpc_lock_client_uses_request_lock_id_and_reports_missing_unlock() {
|
||||||
|
let manager = Arc::new(GlobalLockManager::new());
|
||||||
|
let local_client: Arc<dyn rustfs_lock::LockClient> = Arc::new(LocalClient::with_manager(manager));
|
||||||
|
|
||||||
|
let (addr, handle) = spawn_lock_server(local_client).await.expect("Failed to spawn server");
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
|
||||||
|
let grpc_client = GrpcLockClient::new(addr);
|
||||||
|
let request = LockRequest::new(test_resource(), LockType::Exclusive, "owner-a").with_acquire_timeout(Duration::from_secs(2));
|
||||||
|
|
||||||
|
let response = grpc_client.acquire_lock(&request).await.expect("gRPC acquire should succeed");
|
||||||
|
let lock_info = response.lock_info.expect("gRPC acquire should include lock info");
|
||||||
|
assert_eq!(lock_info.id, request.lock_id);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
grpc_client
|
||||||
|
.release(&request.lock_id)
|
||||||
|
.await
|
||||||
|
.expect("gRPC release should succeed"),
|
||||||
|
"release should find the request lock id"
|
||||||
|
);
|
||||||
|
|
||||||
|
let missing_release = grpc_client
|
||||||
|
.release(&request.lock_id)
|
||||||
|
.await
|
||||||
|
.expect_err("second release should report missing lock");
|
||||||
|
assert!(
|
||||||
|
missing_release.to_string().contains("lock not found for release"),
|
||||||
|
"missing release should preserve server error, got: {missing_release}"
|
||||||
|
);
|
||||||
|
|
||||||
|
handle.abort();
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_distributed_lock_4_nodes_grpc_read_write_quorum_split_with_two_failed_nodes() {
|
async fn test_distributed_lock_4_nodes_grpc_read_write_quorum_split_with_two_failed_nodes() {
|
||||||
let manager1 = Arc::new(GlobalLockManager::new());
|
let manager1 = Arc::new(GlobalLockManager::new());
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ mod get_deleted_object_test;
|
|||||||
mod grpc_lock_client;
|
mod grpc_lock_client;
|
||||||
mod grpc_lock_server;
|
mod grpc_lock_server;
|
||||||
mod head_deleted_object_versioning_test;
|
mod head_deleted_object_versioning_test;
|
||||||
|
mod head_tls_bodyless_test;
|
||||||
mod lifecycle;
|
mod lifecycle;
|
||||||
mod lock;
|
mod lock;
|
||||||
mod node_interact_test;
|
mod node_interact_test;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,259 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use anyhow::{Context, Result, bail};
|
||||||
|
use clap::Parser;
|
||||||
|
use rcgen::{
|
||||||
|
BasicConstraints, CertificateParams, CertifiedIssuer, DnType, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose,
|
||||||
|
SanType,
|
||||||
|
};
|
||||||
|
use std::fs;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use time::{Duration, OffsetDateTime};
|
||||||
|
|
||||||
|
pub const DEFAULT_OUT_DIR: &str = "target/tls";
|
||||||
|
pub const OUTPUT_FILES: [&str; 7] = [
|
||||||
|
"rustfs_cert.pem",
|
||||||
|
"rustfs_key.pem",
|
||||||
|
"ca.crt",
|
||||||
|
"public.crt",
|
||||||
|
"client_ca.crt",
|
||||||
|
"client_cert.pem",
|
||||||
|
"client_key.pem",
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(name = "tls_gen", about = "Generate a full RustFS TLS bundle for local TLS and mTLS tests.")]
|
||||||
|
pub struct Args {
|
||||||
|
#[arg(long, default_value = DEFAULT_OUT_DIR)]
|
||||||
|
pub out_dir: PathBuf,
|
||||||
|
#[arg(long, default_value_t = 365)]
|
||||||
|
pub days: i64,
|
||||||
|
#[arg(long)]
|
||||||
|
pub force: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(args: Args) -> Result<PathBuf> {
|
||||||
|
if args.days <= 0 {
|
||||||
|
bail!("--days must be a positive integer");
|
||||||
|
}
|
||||||
|
|
||||||
|
write_bundle(&args.out_dir, args.force, args.days)?;
|
||||||
|
Ok(args.out_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ensure_writable(out_dir: &Path, force: bool) -> Result<()> {
|
||||||
|
if force {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let existing: Vec<_> = OUTPUT_FILES
|
||||||
|
.iter()
|
||||||
|
.map(|name| out_dir.join(name))
|
||||||
|
.filter(|path| path.exists())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if existing.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let existing_list = existing
|
||||||
|
.iter()
|
||||||
|
.map(|path| path.file_name().and_then(|name| name.to_str()).unwrap_or("<unknown>"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
bail!(
|
||||||
|
"Refusing to overwrite existing files in {}: {}. Re-run with --force to replace them.",
|
||||||
|
out_dir.display(),
|
||||||
|
existing_list
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_bundle(out_dir: &Path, force: bool, days: i64) -> Result<()> {
|
||||||
|
fs::create_dir_all(out_dir).with_context(|| format!("failed to create output directory {}", out_dir.display()))?;
|
||||||
|
ensure_writable(out_dir, force)?;
|
||||||
|
|
||||||
|
let ca_key = generate_private_key()?;
|
||||||
|
let ca = build_ca_certificate(ca_key, days)?;
|
||||||
|
|
||||||
|
let server_key = generate_private_key()?;
|
||||||
|
let server_cert = build_leaf_certificate(
|
||||||
|
&server_key,
|
||||||
|
"localhost",
|
||||||
|
&[SanType::DnsName("localhost".try_into()?)],
|
||||||
|
&[
|
||||||
|
SanType::IpAddress(IpAddr::V4("127.0.0.1".parse()?)),
|
||||||
|
SanType::IpAddress(IpAddr::V6("::1".parse()?)),
|
||||||
|
],
|
||||||
|
ExtendedKeyUsagePurpose::ServerAuth,
|
||||||
|
&ca,
|
||||||
|
days,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let client_key = generate_private_key()?;
|
||||||
|
let client_cert = build_leaf_certificate(
|
||||||
|
&client_key,
|
||||||
|
"rustfs-test-client",
|
||||||
|
&[SanType::DnsName("rustfs-test-client".try_into()?)],
|
||||||
|
&[],
|
||||||
|
ExtendedKeyUsagePurpose::ClientAuth,
|
||||||
|
&ca,
|
||||||
|
days,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let ca_pem = ca.pem();
|
||||||
|
let bundle = [
|
||||||
|
("rustfs_cert.pem", server_cert.pem()),
|
||||||
|
("rustfs_key.pem", server_key.serialize_pem()),
|
||||||
|
("ca.crt", ca_pem.clone()),
|
||||||
|
("public.crt", ca_pem.clone()),
|
||||||
|
("client_ca.crt", ca_pem),
|
||||||
|
("client_cert.pem", client_cert.pem()),
|
||||||
|
("client_key.pem", client_key.serialize_pem()),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (name, content) in bundle {
|
||||||
|
fs::write(out_dir.join(name), content).with_context(|| format!("failed to write {}", out_dir.join(name).display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_ca_certificate(signing_key: KeyPair, days: i64) -> Result<CertifiedIssuer<'static, KeyPair>> {
|
||||||
|
let mut params = base_params("RustFS Test CA", days)?;
|
||||||
|
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
|
||||||
|
params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
|
||||||
|
|
||||||
|
CertifiedIssuer::self_signed(params, signing_key).context("failed to create CA certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_leaf_certificate(
|
||||||
|
signing_key: &KeyPair,
|
||||||
|
common_name: &str,
|
||||||
|
dns_names: &[SanType],
|
||||||
|
ip_addresses: &[SanType],
|
||||||
|
usage: ExtendedKeyUsagePurpose,
|
||||||
|
issuer: &CertifiedIssuer<'_, KeyPair>,
|
||||||
|
days: i64,
|
||||||
|
) -> Result<rcgen::Certificate> {
|
||||||
|
let mut params = base_params(common_name, days)?;
|
||||||
|
params.is_ca = IsCa::ExplicitNoCa;
|
||||||
|
params.key_usages = vec![KeyUsagePurpose::DigitalSignature, KeyUsagePurpose::KeyEncipherment];
|
||||||
|
params.extended_key_usages = vec![usage];
|
||||||
|
params.use_authority_key_identifier_extension = true;
|
||||||
|
params.subject_alt_names.extend_from_slice(dns_names);
|
||||||
|
params.subject_alt_names.extend_from_slice(ip_addresses);
|
||||||
|
|
||||||
|
params
|
||||||
|
.signed_by(signing_key, issuer)
|
||||||
|
.with_context(|| format!("failed to create leaf certificate for {common_name}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_params(common_name: &str, days: i64) -> Result<CertificateParams> {
|
||||||
|
let mut params = CertificateParams::default();
|
||||||
|
let issued_at = OffsetDateTime::now_utc() - Duration::minutes(5);
|
||||||
|
params.not_before = issued_at;
|
||||||
|
params.not_after = issued_at + Duration::days(days);
|
||||||
|
params.distinguished_name.push(DnType::CountryName, "US");
|
||||||
|
params.distinguished_name.push(DnType::OrganizationName, "RustFS");
|
||||||
|
params.distinguished_name.push(DnType::CommonName, common_name);
|
||||||
|
Ok(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_private_key() -> Result<KeyPair> {
|
||||||
|
KeyPair::generate().context("failed to generate private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{Args, OUTPUT_FILES, ensure_writable, run};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
fn unique_temp_dir() -> PathBuf {
|
||||||
|
let suffix = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("system time must be after unix epoch")
|
||||||
|
.as_nanos();
|
||||||
|
std::env::temp_dir().join(format!("rustfs-tls-gen-{suffix}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TempDir {
|
||||||
|
path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TempDir {
|
||||||
|
fn new() -> Self {
|
||||||
|
let path = unique_temp_dir();
|
||||||
|
fs::create_dir_all(&path).expect("temporary directory should be created");
|
||||||
|
Self { path }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self) -> &Path {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TempDir {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = fs::remove_dir_all(&self.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_writes_full_bundle() {
|
||||||
|
let temp_dir = TempDir::new();
|
||||||
|
|
||||||
|
let out_dir = run(Args {
|
||||||
|
out_dir: temp_dir.path().join("tls"),
|
||||||
|
days: 365,
|
||||||
|
force: false,
|
||||||
|
})
|
||||||
|
.expect("bundle generation should succeed");
|
||||||
|
|
||||||
|
for name in OUTPUT_FILES {
|
||||||
|
let content = fs::read(out_dir.join(name)).unwrap_or_else(|error| panic!("{name} should exist: {error}"));
|
||||||
|
assert!(!content.is_empty(), "{name} should not be empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ensure_writable_rejects_existing_files_without_force() {
|
||||||
|
let temp_dir = TempDir::new();
|
||||||
|
let existing = temp_dir.path().join(OUTPUT_FILES[0]);
|
||||||
|
fs::write(&existing, "existing").expect("existing file should be created");
|
||||||
|
|
||||||
|
let error = ensure_writable(temp_dir.path(), false).expect_err("existing files must be rejected");
|
||||||
|
let message = format!("{error:#}");
|
||||||
|
|
||||||
|
assert!(message.contains("Refusing to overwrite existing files"));
|
||||||
|
assert!(message.contains(OUTPUT_FILES[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_rejects_non_positive_days() {
|
||||||
|
let temp_dir = TempDir::new();
|
||||||
|
let error = run(Args {
|
||||||
|
out_dir: temp_dir.path().join("tls"),
|
||||||
|
days: 0,
|
||||||
|
force: false,
|
||||||
|
})
|
||||||
|
.expect_err("non-positive days must fail");
|
||||||
|
|
||||||
|
assert_eq!(format!("{error:#}"), "--days must be a positive integer");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,7 @@ flatbuffers.workspace = true
|
|||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
futures-util.workspace = true
|
futures-util.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
tracing-opentelemetry.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
time.workspace = true
|
time.workspace = true
|
||||||
bytesize.workspace = true
|
bytesize.workspace = true
|
||||||
@@ -62,6 +63,7 @@ serde_json.workspace = true
|
|||||||
quick-xml = { workspace = true, features = ["serialize", "async-tokio"] }
|
quick-xml = { workspace = true, features = ["serialize", "async-tokio"] }
|
||||||
s3s.workspace = true
|
s3s.workspace = true
|
||||||
http.workspace = true
|
http.workspace = true
|
||||||
|
opentelemetry.workspace = true
|
||||||
http-body = { workspace = true }
|
http-body = { workspace = true }
|
||||||
http-body-util.workspace = true
|
http-body-util.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
@@ -98,6 +100,7 @@ pin-project-lite.workspace = true
|
|||||||
md-5.workspace = true
|
md-5.workspace = true
|
||||||
memmap2 = { workspace = true }
|
memmap2 = { workspace = true }
|
||||||
libc.workspace = true
|
libc.workspace = true
|
||||||
|
rustix = { workspace = true }
|
||||||
rustfs-madmin.workspace = true
|
rustfs-madmin.workspace = true
|
||||||
rustfs-workers.workspace = true
|
rustfs-workers.workspace = true
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
@@ -124,9 +127,10 @@ metrics = { workspace = true }
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||||
criterion = { workspace = true, features = ["html_reports"] }
|
criterion = { workspace = true, features = ["html_reports"] }
|
||||||
temp-env = { workspace = true }
|
temp-env = { workspace = true, features = ["async_closure"] }
|
||||||
tracing-subscriber = { workspace = true }
|
tracing-subscriber = { workspace = true }
|
||||||
serial_test = { workspace = true }
|
serial_test = { workspace = true }
|
||||||
|
opentelemetry_sdk = { workspace = true }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
shadow-rs = { workspace = true, features = ["build", "metadata"] }
|
shadow-rs = { workspace = true, features = ["build", "metadata"] }
|
||||||
|
|||||||
@@ -183,11 +183,8 @@ pub async fn get_local_server_property() -> ServerProperties {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// let mut sensitive = HashSet::new();
|
// let mut sensitive = HashSet::new();
|
||||||
// sensitive.insert(ENV_ACCESS_KEY.to_string());
|
// sensitive.insert(rustfs_config::ENV_RUSTFS_ACCESS_KEY.to_string());
|
||||||
// sensitive.insert(ENV_SECRET_KEY.to_string());
|
// sensitive.insert(rustfs_config::ENV_RUSTFS_SECRET_KEY.to_string());
|
||||||
// sensitive.insert(ENV_ROOT_USER.to_string());
|
|
||||||
// sensitive.insert(ENV_ROOT_PASSWORD.to_string());
|
|
||||||
|
|
||||||
if let Some(store) = new_object_layer_fn() {
|
if let Some(store) = new_object_layer_fn() {
|
||||||
let storage_info = store.local_storage_info().await;
|
let storage_info = store.local_storage_info().await;
|
||||||
props.state = ITEM_ONLINE.to_string();
|
props.state = ITEM_ONLINE.to_string();
|
||||||
|
|||||||
@@ -87,25 +87,70 @@ impl AsyncBatchProcessor {
|
|||||||
T: Send + 'static,
|
T: Send + 'static,
|
||||||
F: Future<Output = Result<T>> + Send + 'static,
|
F: Future<Output = Result<T>> + Send + 'static,
|
||||||
{
|
{
|
||||||
let results = self.execute_batch(tasks).await;
|
if required_successes == 0 {
|
||||||
let mut successes = Vec::new();
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
for value in results.into_iter().flatten() {
|
if tasks.is_empty() {
|
||||||
successes.push(value);
|
return Err(Error::other(format!(
|
||||||
if successes.len() >= required_successes {
|
"Insufficient successful results: got 0, needed {required_successes}"
|
||||||
return Ok(successes);
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let semaphore = Arc::new(tokio::sync::Semaphore::new(self.max_concurrent));
|
||||||
|
let mut join_set = JoinSet::new();
|
||||||
|
let mut successes = Vec::new();
|
||||||
|
let mut pending_tasks = tasks.len();
|
||||||
|
let mut first_error = None;
|
||||||
|
|
||||||
|
for task in tasks {
|
||||||
|
let sem = semaphore.clone();
|
||||||
|
join_set.spawn(async move {
|
||||||
|
let _permit = sem.acquire().await.map_err(|_| Error::other("Semaphore error"))?;
|
||||||
|
task.await
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(join_result) = join_set.join_next().await {
|
||||||
|
pending_tasks = pending_tasks.saturating_sub(1);
|
||||||
|
|
||||||
|
match join_result {
|
||||||
|
Ok(Ok(value)) => {
|
||||||
|
successes.push(value);
|
||||||
|
if successes.len() >= required_successes {
|
||||||
|
return Ok(successes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Err(err)) => {
|
||||||
|
if first_error.is_none() {
|
||||||
|
first_error = Some(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(join_error) => {
|
||||||
|
if first_error.is_none() {
|
||||||
|
first_error = Some(Error::other(format!("Task panicked in quorum batch processor: {join_error}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if successes.len() + pending_tasks < required_successes {
|
||||||
|
return Err(first_error.unwrap_or_else(|| {
|
||||||
|
Error::other(format!(
|
||||||
|
"Insufficient successful results: got {}, needed {}",
|
||||||
|
successes.len(),
|
||||||
|
required_successes
|
||||||
|
))
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if successes.len() >= required_successes {
|
Err(first_error.unwrap_or_else(|| {
|
||||||
Ok(successes)
|
Error::other(format!(
|
||||||
} else {
|
|
||||||
Err(Error::other(format!(
|
|
||||||
"Insufficient successful results: got {}, needed {}",
|
"Insufficient successful results: got {}, needed {}",
|
||||||
successes.len(),
|
successes.len(),
|
||||||
required_successes
|
required_successes
|
||||||
)))
|
))
|
||||||
}
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,4 +273,52 @@ mod tests {
|
|||||||
let successes = results.unwrap();
|
let successes = results.unwrap();
|
||||||
assert!(successes.len() >= 2);
|
assert!(successes.len() >= 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_batch_processor_quorum_returns_before_slow_tail() {
|
||||||
|
let processor = AsyncBatchProcessor::new(4);
|
||||||
|
let started = std::time::Instant::now();
|
||||||
|
|
||||||
|
let tasks: Vec<_> = [(10_u64, Ok(1_i32)), (15, Ok(2)), (250, Ok(3))]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(delay_ms, outcome)| async move {
|
||||||
|
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
|
||||||
|
outcome
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let results = processor
|
||||||
|
.execute_batch_with_quorum(tasks, 2)
|
||||||
|
.await
|
||||||
|
.expect("quorum should succeed");
|
||||||
|
assert_eq!(results.len(), 2);
|
||||||
|
assert!(started.elapsed() < Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_batch_processor_quorum_fails_once_quorum_becomes_impossible() {
|
||||||
|
let processor = AsyncBatchProcessor::new(4);
|
||||||
|
let started = std::time::Instant::now();
|
||||||
|
|
||||||
|
let tasks: Vec<_> = vec![
|
||||||
|
(10_u64, Ok(1_i32)),
|
||||||
|
(15, Err(Error::other("first failure"))),
|
||||||
|
(20, Err(Error::other("second failure"))),
|
||||||
|
(250, Ok(4)),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(delay_ms, outcome)| async move {
|
||||||
|
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
|
||||||
|
outcome
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let err = processor
|
||||||
|
.execute_batch_with_quorum(tasks, 3)
|
||||||
|
.await
|
||||||
|
.expect_err("quorum should fail once it becomes impossible");
|
||||||
|
|
||||||
|
assert!(err.to_string().contains("first failure"));
|
||||||
|
assert!(started.elapsed() < Duration::from_millis(120));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -395,8 +395,27 @@ impl BucketTargetSys {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_target(&self, bucket: &str, target: &BucketTarget, update: bool) -> Result<(), BucketTargetError> {
|
pub async fn set_target(
|
||||||
if !target.target_type.is_valid() && !update {
|
&self,
|
||||||
|
bucket: &str,
|
||||||
|
target: &BucketTarget,
|
||||||
|
update: bool,
|
||||||
|
) -> Result<BucketTargets, BucketTargetError> {
|
||||||
|
self.validate_target(bucket, target).await?;
|
||||||
|
|
||||||
|
let mut bucket_targets = match self.list_bucket_targets(bucket).await {
|
||||||
|
Ok(targets) => targets,
|
||||||
|
Err(BucketTargetError::BucketRemoteTargetNotFound { .. }) => BucketTargets::default(),
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
};
|
||||||
|
|
||||||
|
Self::upsert_target_entry(&mut bucket_targets.targets, target, update)?;
|
||||||
|
|
||||||
|
Ok(bucket_targets)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn validate_target(&self, bucket: &str, target: &BucketTarget) -> Result<(), BucketTargetError> {
|
||||||
|
if !target.target_type.is_valid() {
|
||||||
return Err(BucketTargetError::BucketRemoteArnTypeInvalid {
|
return Err(BucketTargetError::BucketRemoteArnTypeInvalid {
|
||||||
bucket: bucket.to_string(),
|
bucket: bucket.to_string(),
|
||||||
});
|
});
|
||||||
@@ -450,52 +469,44 @@ impl BucketTargetSys {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
Ok(())
|
||||||
let mut targets_map = self.targets_map.write().await;
|
}
|
||||||
let bucket_targets = targets_map.entry(bucket.to_string()).or_insert_with(Vec::new);
|
|
||||||
let mut found = false;
|
|
||||||
|
|
||||||
for (idx, existing_target) in bucket_targets.iter().enumerate() {
|
fn upsert_target_entry(
|
||||||
if existing_target.target_type.to_string() == target.target_type.to_string() {
|
bucket_targets: &mut Vec<BucketTarget>,
|
||||||
if existing_target.arn == target.arn {
|
target: &BucketTarget,
|
||||||
if !update {
|
update: bool,
|
||||||
return Err(BucketTargetError::BucketRemoteAlreadyExists {
|
) -> Result<(), BucketTargetError> {
|
||||||
bucket: existing_target.target_bucket.clone(),
|
let mut found = false;
|
||||||
});
|
|
||||||
}
|
for (idx, existing_target) in bucket_targets.iter().enumerate() {
|
||||||
bucket_targets[idx] = target.clone();
|
if existing_target.target_type.to_string() == target.target_type.to_string() {
|
||||||
found = true;
|
if existing_target.arn == target.arn {
|
||||||
break;
|
if !update {
|
||||||
}
|
|
||||||
if existing_target.endpoint == target.endpoint {
|
|
||||||
return Err(BucketTargetError::BucketRemoteAlreadyExists {
|
return Err(BucketTargetError::BucketRemoteAlreadyExists {
|
||||||
bucket: existing_target.target_bucket.clone(),
|
bucket: existing_target.target_bucket.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
bucket_targets[idx] = target.clone();
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if existing_target.endpoint == target.endpoint {
|
||||||
|
return Err(BucketTargetError::BucketRemoteAlreadyExists {
|
||||||
|
bucket: existing_target.target_bucket.clone(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found && !update {
|
|
||||||
bucket_targets.push(target.clone());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
if !found && !update {
|
||||||
let mut arn_remotes_map = self.arn_remotes_map.write().await;
|
bucket_targets.push(target.clone());
|
||||||
arn_remotes_map.insert(
|
|
||||||
target.arn.clone(),
|
|
||||||
ArnTarget {
|
|
||||||
client: Some(Arc::new(target_client)),
|
|
||||||
last_refresh: OffsetDateTime::now_utc(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update_bandwidth_limit(bucket, &target.arn, target.bandwidth_limit);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn remove_target(&self, bucket: &str, arn_str: &str) -> Result<(), BucketTargetError> {
|
pub async fn remove_target(&self, bucket: &str, arn_str: &str) -> Result<BucketTargets, BucketTargetError> {
|
||||||
if arn_str.is_empty() {
|
if arn_str.is_empty() {
|
||||||
return Err(BucketTargetError::BucketRemoteArnInvalid {
|
return Err(BucketTargetError::BucketRemoteArnInvalid {
|
||||||
bucket: bucket.to_string(),
|
bucket: bucket.to_string(),
|
||||||
@@ -524,33 +535,16 @@ impl BucketTargetSys {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
let targets = self.list_bucket_targets(bucket).await?;
|
||||||
let mut targets_map = self.targets_map.write().await;
|
let new_targets: Vec<BucketTarget> = targets.targets.iter().filter(|t| t.arn != arn_str).cloned().collect();
|
||||||
|
|
||||||
let Some(targets) = targets_map.get(bucket) else {
|
if new_targets.len() == targets.targets.len() {
|
||||||
return Err(BucketTargetError::BucketRemoteTargetNotFound {
|
return Err(BucketTargetError::BucketRemoteTargetNotFound {
|
||||||
bucket: bucket.to_string(),
|
bucket: bucket.to_string(),
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
let new_targets: Vec<BucketTarget> = targets.iter().filter(|t| t.arn != arn_str).cloned().collect();
|
|
||||||
|
|
||||||
if new_targets.len() == targets.len() {
|
|
||||||
return Err(BucketTargetError::BucketRemoteTargetNotFound {
|
|
||||||
bucket: bucket.to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
targets_map.insert(bucket.to_string(), new_targets);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
Ok(BucketTargets { targets: new_targets })
|
||||||
self.arn_remotes_map.write().await.remove(arn_str);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.update_bandwidth_limit(bucket, arn_str, 0);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn mark_refresh_in_progress(&self, bucket: &str, arn: &str) {
|
pub async fn mark_refresh_in_progress(&self, bucket: &str, arn: &str) {
|
||||||
@@ -603,7 +597,7 @@ impl BucketTargetSys {
|
|||||||
|
|
||||||
if let Some(last_refresh) = last_refresh {
|
if let Some(last_refresh) = last_refresh {
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
if now - last_refresh > Duration::from_secs(60 * 5) {
|
if now - last_refresh < Duration::from_secs(60 * 5) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -619,6 +613,16 @@ impl BucketTargetSys {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let cli = self
|
||||||
|
.arn_remotes_map
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.get(arn)
|
||||||
|
.and_then(|target| target.client.clone());
|
||||||
|
if cli.is_some() {
|
||||||
|
return cli;
|
||||||
|
}
|
||||||
|
|
||||||
self.inc_arn_errs(bucket, arn).await;
|
self.inc_arn_errs(bucket, arn).await;
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ use rustfs_filemeta::{
|
|||||||
use rustfs_s3_common::EventName;
|
use rustfs_s3_common::EventName;
|
||||||
use rustfs_utils::{get_env_i64, get_env_usize, path::encode_dir_object, string::strings_has_prefix_fold};
|
use rustfs_utils::{get_env_i64, get_env_usize, path::encode_dir_object, string::strings_has_prefix_fold};
|
||||||
use s3s::dto::{
|
use s3s::dto::{
|
||||||
BucketLifecycleConfiguration, DefaultRetention, ReplicationConfiguration, RestoreRequest, RestoreRequestType, RestoreStatus,
|
BucketLifecycleConfiguration, DefaultRetention, ExpirationStatus, ReplicationConfiguration, RestoreRequest,
|
||||||
Timestamp,
|
RestoreRequestType, RestoreStatus, Timestamp,
|
||||||
};
|
};
|
||||||
use s3s::header::{X_AMZ_RESTORE, X_AMZ_SERVER_SIDE_ENCRYPTION};
|
use s3s::header::{X_AMZ_RESTORE, X_AMZ_SERVER_SIDE_ENCRYPTION};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
@@ -92,6 +92,7 @@ const ENV_STALE_UPLOADS_EXPIRY: &str = "RUSTFS_API_STALE_UPLOADS_EXPIRY";
|
|||||||
const ENV_STALE_UPLOADS_CLEANUP_INTERVAL: &str = "RUSTFS_API_STALE_UPLOADS_CLEANUP_INTERVAL";
|
const ENV_STALE_UPLOADS_CLEANUP_INTERVAL: &str = "RUSTFS_API_STALE_UPLOADS_CLEANUP_INTERVAL";
|
||||||
const DEFAULT_STALE_UPLOADS_EXPIRY: StdDuration = StdDuration::from_secs(24 * 60 * 60);
|
const DEFAULT_STALE_UPLOADS_EXPIRY: StdDuration = StdDuration::from_secs(24 * 60 * 60);
|
||||||
const DEFAULT_STALE_UPLOADS_CLEANUP_INTERVAL: StdDuration = StdDuration::from_secs(6 * 60 * 60);
|
const DEFAULT_STALE_UPLOADS_CLEANUP_INTERVAL: StdDuration = StdDuration::from_secs(6 * 60 * 60);
|
||||||
|
const DATE_EXPIRY_EXISTING_OBJECTS_GRACE_SECS: i64 = 5;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref GLOBAL_ExpiryState: Arc<RwLock<ExpiryState>> = ExpiryState::new();
|
pub static ref GLOBAL_ExpiryState: Arc<RwLock<ExpiryState>> = ExpiryState::new();
|
||||||
@@ -1241,6 +1242,29 @@ pub async fn enqueue_transition_for_existing_objects(api: Arc<ECStore>, bucket:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lifecycle_rule_has_date_expiration(lc: &BucketLifecycleConfiguration, rule_id: &str) -> bool {
|
||||||
|
lc.rules.iter().any(|rule| {
|
||||||
|
rule.status == ExpirationStatus::from_static(ExpirationStatus::ENABLED)
|
||||||
|
&& rule.id.as_deref() == Some(rule_id)
|
||||||
|
&& rule.expiration.as_ref().is_some_and(|expiration| expiration.date.is_some())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_defer_date_expiry_for_recent_config_update(lc: &BucketLifecycleConfiguration, now: OffsetDateTime) -> bool {
|
||||||
|
lc.expiry_updated_at.as_ref().is_some_and(|updated_at| {
|
||||||
|
let updated_at = OffsetDateTime::from(updated_at.clone());
|
||||||
|
now.unix_timestamp().saturating_sub(updated_at.unix_timestamp()) < DATE_EXPIRY_EXISTING_OBJECTS_GRACE_SECS
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn apply_existing_object_expiry(api: Arc<ECStore>, object: &ObjectInfo, event: &lifecycle::Event, src: &LcEventSrc) {
|
||||||
|
if object.is_remote() {
|
||||||
|
apply_expiry_on_transitioned_object(api, object, event, src).await;
|
||||||
|
} else {
|
||||||
|
apply_expiry_on_non_transitioned_objects(api, object, event, src).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn enqueue_expiry_for_existing_objects(api: Arc<ECStore>, bucket: &str) -> Result<(), Error> {
|
pub async fn enqueue_expiry_for_existing_objects(api: Arc<ECStore>, bucket: &str) -> Result<(), Error> {
|
||||||
let Ok((lc, _)) = metadata_sys::get_lifecycle_config(bucket).await else {
|
let Ok((lc, _)) = metadata_sys::get_lifecycle_config(bucket).await else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -1253,6 +1277,8 @@ pub async fn enqueue_expiry_for_existing_objects(api: Arc<ECStore>, bucket: &str
|
|||||||
let mut marker = None;
|
let mut marker = None;
|
||||||
let mut version_marker = None;
|
let mut version_marker = None;
|
||||||
let src = LcEventSrc::Scanner;
|
let src = LcEventSrc::Scanner;
|
||||||
|
let defer_date_expiry_once = should_defer_date_expiry_for_recent_config_update(&lc, OffsetDateTime::now_utc());
|
||||||
|
let mut date_expiry_deferred_once = false;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let page = api
|
let page = api
|
||||||
@@ -1269,15 +1295,16 @@ pub async fn enqueue_expiry_for_existing_objects(api: Arc<ECStore>, bucket: &str
|
|||||||
| IlmAction::DeleteRestoredVersionAction
|
| IlmAction::DeleteRestoredVersionAction
|
||||||
| IlmAction::DeleteAllVersionsAction
|
| IlmAction::DeleteAllVersionsAction
|
||||||
| IlmAction::DelMarkerDeleteAllVersionsAction => {
|
| IlmAction::DelMarkerDeleteAllVersionsAction => {
|
||||||
if event
|
let now = OffsetDateTime::now_utc();
|
||||||
.due
|
if event.due.is_some_and(|due| due.unix_timestamp() <= now.unix_timestamp()) {
|
||||||
.is_some_and(|due| due.unix_timestamp() <= OffsetDateTime::now_utc().unix_timestamp())
|
if defer_date_expiry_once
|
||||||
{
|
&& !date_expiry_deferred_once
|
||||||
if object.is_remote() {
|
&& lifecycle_rule_has_date_expiration(&lc, &event.rule_id)
|
||||||
apply_expiry_on_transitioned_object(api.clone(), object, &event, &src).await;
|
{
|
||||||
} else {
|
tokio::time::sleep(StdDuration::from_secs(DATE_EXPIRY_EXISTING_OBJECTS_GRACE_SECS as u64)).await;
|
||||||
apply_expiry_on_non_transitioned_objects(api.clone(), object, &event, &src).await;
|
date_expiry_deferred_once = true;
|
||||||
}
|
}
|
||||||
|
apply_existing_object_expiry(api.clone(), object, &event, &src).await;
|
||||||
} else {
|
} else {
|
||||||
apply_expiry_rule(&event, &src, object).await;
|
apply_expiry_rule(&event, &src, object).await;
|
||||||
}
|
}
|
||||||
@@ -1977,9 +2004,10 @@ pub async fn apply_lifecycle_action(event: &lifecycle::Event, src: &LcEventSrc,
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
StaleMultipartUploadCandidate, cleanup_empty_multipart_sha_dirs_on_local_disks, cleanup_stale_multipart_uploads_once_at,
|
DATE_EXPIRY_EXISTING_OBJECTS_GRACE_SECS, StaleMultipartUploadCandidate, cleanup_empty_multipart_sha_dirs_on_local_disks,
|
||||||
lifecycle_deleted_object, lifecycle_version_purge_state_from_completed_targets,
|
cleanup_stale_multipart_uploads_once_at, lifecycle_deleted_object, lifecycle_rule_has_date_expiration,
|
||||||
mark_delete_opts_skip_decommissioned_on_remote_success, merge_stale_multipart_candidate, replication_state_for_delete,
|
lifecycle_version_purge_state_from_completed_targets, mark_delete_opts_skip_decommissioned_on_remote_success,
|
||||||
|
merge_stale_multipart_candidate, replication_state_for_delete, should_defer_date_expiry_for_recent_config_update,
|
||||||
should_reuse_lifecycle_delete_replication_state,
|
should_reuse_lifecycle_delete_replication_state,
|
||||||
};
|
};
|
||||||
use crate::bucket::metadata::BUCKET_LIFECYCLE_CONFIG;
|
use crate::bucket::metadata::BUCKET_LIFECYCLE_CONFIG;
|
||||||
@@ -1994,6 +2022,7 @@ mod tests {
|
|||||||
BucketOperations, BucketOptions, MakeBucketOptions, MultipartOperations, ObjectInfo, ObjectOptions, PutObjReader,
|
BucketOperations, BucketOptions, MakeBucketOptions, MultipartOperations, ObjectInfo, ObjectOptions, PutObjReader,
|
||||||
};
|
};
|
||||||
use rustfs_filemeta::{ReplicateDecision, VersionPurgeStatusType};
|
use rustfs_filemeta::{ReplicateDecision, VersionPurgeStatusType};
|
||||||
|
use s3s::dto::{BucketLifecycleConfiguration, ExpirationStatus, LifecycleExpiration, LifecycleRule, Timestamp};
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -2014,6 +2043,49 @@ mod tests {
|
|||||||
assert!(opts.skip_decommissioned);
|
assert!(opts.skip_decommissioned);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lifecycle_rule_has_date_expiration_detects_enabled_date_rule() {
|
||||||
|
let lc = BucketLifecycleConfiguration {
|
||||||
|
expiry_updated_at: None,
|
||||||
|
rules: vec![LifecycleRule {
|
||||||
|
status: ExpirationStatus::from_static(ExpirationStatus::ENABLED),
|
||||||
|
expiration: Some(LifecycleExpiration {
|
||||||
|
date: Some(Timestamp::from(OffsetDateTime::now_utc())),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
id: Some("rule-date".to_string()),
|
||||||
|
abort_incomplete_multipart_upload: None,
|
||||||
|
del_marker_expiration: None,
|
||||||
|
filter: None,
|
||||||
|
noncurrent_version_expiration: None,
|
||||||
|
noncurrent_version_transitions: None,
|
||||||
|
prefix: None,
|
||||||
|
transitions: None,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(lifecycle_rule_has_date_expiration(&lc, "rule-date"));
|
||||||
|
assert!(!lifecycle_rule_has_date_expiration(&lc, "missing-rule"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_defer_date_expiry_for_recent_config_update_respects_grace_window() {
|
||||||
|
let now = OffsetDateTime::now_utc();
|
||||||
|
let recent = BucketLifecycleConfiguration {
|
||||||
|
expiry_updated_at: Some(Timestamp::from(now - time::Duration::seconds(1))),
|
||||||
|
rules: Vec::new(),
|
||||||
|
};
|
||||||
|
let stale = BucketLifecycleConfiguration {
|
||||||
|
expiry_updated_at: Some(Timestamp::from(
|
||||||
|
now - time::Duration::seconds(DATE_EXPIRY_EXISTING_OBJECTS_GRACE_SECS + 1),
|
||||||
|
)),
|
||||||
|
rules: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(should_defer_date_expiry_for_recent_config_update(&recent, now));
|
||||||
|
assert!(!should_defer_date_expiry_for_recent_config_update(&stale, now));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mark_delete_opts_skip_decommissioned_on_remote_success_preserves_false_on_failure() {
|
fn mark_delete_opts_skip_decommissioned_on_remote_success_preserves_false_on_failure() {
|
||||||
let mut opts = ObjectOptions::default();
|
let mut opts = ObjectOptions::default();
|
||||||
|
|||||||
@@ -20,15 +20,144 @@
|
|||||||
|
|
||||||
use crate::bucket::lifecycle::bucket_lifecycle_ops::{ExpiryOp, GLOBAL_ExpiryState, TransitionedObject};
|
use crate::bucket::lifecycle::bucket_lifecycle_ops::{ExpiryOp, GLOBAL_ExpiryState, TransitionedObject};
|
||||||
use crate::bucket::lifecycle::lifecycle::{self, ObjectOpts};
|
use crate::bucket::lifecycle::lifecycle::{self, ObjectOpts};
|
||||||
|
use crate::client::signer_error::error_chain_contains_signer_header_marker;
|
||||||
use crate::global::GLOBAL_TierConfigMgr;
|
use crate::global::GLOBAL_TierConfigMgr;
|
||||||
|
use rustfs_utils::get_env_usize;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
use std::collections::VecDeque;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use tokio::sync::{Mutex, Semaphore};
|
||||||
|
use tracing::warn;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use xxhash_rust::xxh64;
|
use xxhash_rust::xxh64;
|
||||||
|
|
||||||
static XXHASH_SEED: u64 = 0;
|
static XXHASH_SEED: u64 = 0;
|
||||||
|
|
||||||
|
const ENV_REMOTE_DELETE_MAX_CONCURRENCY: &str = "RUSTFS_REMOTE_DELETE_MAX_CONCURRENCY";
|
||||||
|
const ENV_REMOTE_DELETE_BREAKER_THRESHOLD: &str = "RUSTFS_REMOTE_DELETE_BREAKER_THRESHOLD";
|
||||||
|
const ENV_REMOTE_DELETE_BREAKER_WINDOW_SECS: &str = "RUSTFS_REMOTE_DELETE_BREAKER_WINDOW_SECS";
|
||||||
|
const DEFAULT_REMOTE_DELETE_BREAKER_THRESHOLD: usize = 50;
|
||||||
|
const DEFAULT_REMOTE_DELETE_BREAKER_WINDOW_SECS: usize = 30;
|
||||||
|
const METRIC_DELETE_REMOTE_FAILED_TOTAL: &str = "rustfs_delete_remote_failed_total";
|
||||||
|
const METRIC_DELETE_REMOTE_BREAKER_TOTAL: &str = "rustfs_delete_remote_breaker_total";
|
||||||
|
const METRIC_DELETE_REMOTE_INFLIGHT: &str = "rustfs_delete_remote_inflight";
|
||||||
|
|
||||||
|
static REMOTE_DELETE_INFLIGHT: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
|
static REMOTE_DELETE_LIMITER: LazyLock<Semaphore> = LazyLock::new(|| {
|
||||||
|
let default_limit = std::cmp::min(num_cpus::get(), 16).max(1);
|
||||||
|
let concurrency = get_env_usize(ENV_REMOTE_DELETE_MAX_CONCURRENCY, default_limit).max(1);
|
||||||
|
Semaphore::new(concurrency)
|
||||||
|
});
|
||||||
|
|
||||||
|
static REMOTE_DELETE_BREAKER: LazyLock<Mutex<RemoteDeleteBreaker>> = LazyLock::new(|| {
|
||||||
|
Mutex::new(RemoteDeleteBreaker::new(
|
||||||
|
get_env_usize(ENV_REMOTE_DELETE_BREAKER_THRESHOLD, DEFAULT_REMOTE_DELETE_BREAKER_THRESHOLD).max(1),
|
||||||
|
Duration::from_secs(
|
||||||
|
get_env_usize(ENV_REMOTE_DELETE_BREAKER_WINDOW_SECS, DEFAULT_REMOTE_DELETE_BREAKER_WINDOW_SECS) as u64,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct RemoteDeleteBreaker {
|
||||||
|
threshold: usize,
|
||||||
|
window: Duration,
|
||||||
|
failures: VecDeque<Instant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RemoteDeleteBreaker {
|
||||||
|
fn new(threshold: usize, window: Duration) -> Self {
|
||||||
|
Self {
|
||||||
|
threshold: threshold.max(1),
|
||||||
|
window: window.max(Duration::from_secs(1)),
|
||||||
|
failures: VecDeque::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_short_circuit(&mut self, now: Instant) -> bool {
|
||||||
|
self.prune(now);
|
||||||
|
self.failures.len() >= self.threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_signer_failure(&mut self, now: Instant) -> bool {
|
||||||
|
self.prune(now);
|
||||||
|
let was_open = self.failures.len() >= self.threshold;
|
||||||
|
self.failures.push_back(now);
|
||||||
|
!was_open && self.failures.len() >= self.threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prune(&mut self, now: Instant) {
|
||||||
|
while let Some(ts) = self.failures.front().copied() {
|
||||||
|
if now.duration_since(ts) > self.window {
|
||||||
|
self.failures.pop_front();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RemoteDeleteInflightGuard;
|
||||||
|
|
||||||
|
impl RemoteDeleteInflightGuard {
|
||||||
|
fn new() -> Self {
|
||||||
|
let inflight = REMOTE_DELETE_INFLIGHT.fetch_add(1, Ordering::Relaxed) + 1;
|
||||||
|
metrics::gauge!(METRIC_DELETE_REMOTE_INFLIGHT).set(inflight as f64);
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for RemoteDeleteInflightGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let inflight = REMOTE_DELETE_INFLIGHT.fetch_sub(1, Ordering::Relaxed) - 1;
|
||||||
|
metrics::gauge!(METRIC_DELETE_REMOTE_INFLIGHT).set(inflight as f64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_signer_header_error(err: &std::io::Error) -> bool {
|
||||||
|
if err.kind() != std::io::ErrorKind::InvalidInput {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(source) = err.get_ref() {
|
||||||
|
if error_chain_contains_signer_header_marker(source) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = err.to_string().to_ascii_lowercase();
|
||||||
|
message.contains("invalid utf-8 header value")
|
||||||
|
|| message.contains("invalidheadervalue")
|
||||||
|
|| (message.contains("sign v4") && message.contains("header value"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remote_delete_breaker_is_open(now: Instant) -> bool {
|
||||||
|
let mut breaker = REMOTE_DELETE_BREAKER.lock().await;
|
||||||
|
breaker.should_short_circuit(now)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn record_remote_delete_failure(err: &std::io::Error, now: Instant) {
|
||||||
|
metrics::counter!(METRIC_DELETE_REMOTE_FAILED_TOTAL).increment(1);
|
||||||
|
|
||||||
|
if !is_signer_header_error(err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut breaker = REMOTE_DELETE_BREAKER.lock().await;
|
||||||
|
if breaker.record_signer_failure(now) {
|
||||||
|
warn!(
|
||||||
|
threshold = breaker.threshold,
|
||||||
|
window_secs = breaker.window.as_secs(),
|
||||||
|
"remote tier delete breaker opened by signer/header failures"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
struct ObjSweeper {
|
struct ObjSweeper {
|
||||||
@@ -148,12 +277,31 @@ impl ExpiryOp for Jentry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_object_from_remote_tier(obj_name: &str, rv_id: &str, tier_name: &str) -> Result<(), std::io::Error> {
|
pub async fn delete_object_from_remote_tier(obj_name: &str, rv_id: &str, tier_name: &str) -> Result<(), std::io::Error> {
|
||||||
|
if remote_delete_breaker_is_open(Instant::now()).await {
|
||||||
|
metrics::counter!(METRIC_DELETE_REMOTE_BREAKER_TOTAL).increment(1);
|
||||||
|
return Err(std::io::Error::other("remote tier delete breaker is open due to signer/header failures"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let _permit = REMOTE_DELETE_LIMITER
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.map_err(|_| std::io::Error::other("remote tier delete limiter is closed"))?;
|
||||||
|
let _inflight = RemoteDeleteInflightGuard::new();
|
||||||
|
|
||||||
let mut config_mgr = GLOBAL_TierConfigMgr.write().await;
|
let mut config_mgr = GLOBAL_TierConfigMgr.write().await;
|
||||||
let w = match config_mgr.get_driver(tier_name).await {
|
let w = match config_mgr.get_driver(tier_name).await {
|
||||||
Ok(w) => w,
|
Ok(w) => w,
|
||||||
Err(e) => return Err(std::io::Error::other(e)),
|
Err(e) => {
|
||||||
|
let err = std::io::Error::other(e);
|
||||||
|
record_remote_delete_failure(&err, Instant::now()).await;
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
w.remove(obj_name, rv_id).await
|
let result = w.remove(obj_name, rv_id).await;
|
||||||
|
if let Err(err) = &result {
|
||||||
|
record_remote_delete_failure(err, Instant::now()).await;
|
||||||
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn transitioned_delete_journal_entry(
|
pub fn transitioned_delete_journal_entry(
|
||||||
@@ -189,4 +337,44 @@ pub fn transitioned_force_delete_journal_entry(transitioned: &TransitionedObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {}
|
mod test {
|
||||||
|
use crate::client::signer_error::invalid_utf8_header_error;
|
||||||
|
|
||||||
|
use super::{RemoteDeleteBreaker, is_signer_header_error};
|
||||||
|
use std::io::{Error, ErrorKind};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn signer_header_error_detection_matches_utf8_failures() {
|
||||||
|
let err = Error::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"failed to sign v4 request: invalid UTF-8 header value for `x-amz-meta-invalid`",
|
||||||
|
);
|
||||||
|
assert!(is_signer_header_error(&err));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn signer_header_error_detection_rejects_unrelated_errors() {
|
||||||
|
let err = Error::other("dial tcp: i/o timeout");
|
||||||
|
assert!(!is_signer_header_error(&err));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn signer_header_error_detection_matches_structured_marker() {
|
||||||
|
let err = invalid_utf8_header_error("failed to sign v4 request", "x-amz-meta-invalid");
|
||||||
|
assert!(is_signer_header_error(&err));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn breaker_opens_at_threshold_and_recovers_after_window() {
|
||||||
|
let mut breaker = RemoteDeleteBreaker::new(3, Duration::from_secs(30));
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
assert!(!breaker.should_short_circuit(start));
|
||||||
|
assert!(!breaker.record_signer_failure(start));
|
||||||
|
assert!(!breaker.record_signer_failure(start + Duration::from_secs(1)));
|
||||||
|
assert!(breaker.record_signer_failure(start + Duration::from_secs(2)));
|
||||||
|
assert!(breaker.should_short_circuit(start + Duration::from_secs(3)));
|
||||||
|
assert!(!breaker.should_short_circuit(start + Duration::from_secs(40)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -224,19 +224,98 @@ impl ReplicationConfigurationExt for ReplicationConfiguration {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.role.is_empty() {
|
if !rule.destination.bucket.is_empty() && !targets_map.contains(&rule.destination.bucket) {
|
||||||
arns.push(self.role.clone()); // Use the legacy RoleArn when present
|
|
||||||
return arns;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !targets_map.contains(&rule.destination.bucket) {
|
|
||||||
targets_map.insert(rule.destination.bucket.clone());
|
targets_map.insert(rule.destination.bucket.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if targets_map.is_empty() && !self.role.is_empty() {
|
||||||
|
arns.push(self.role.clone());
|
||||||
|
return arns;
|
||||||
|
}
|
||||||
|
|
||||||
for arn in targets_map {
|
for arn in targets_map {
|
||||||
arns.push(arn);
|
arns.push(arn);
|
||||||
}
|
}
|
||||||
arns
|
arns
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use s3s::dto::{DeleteMarkerReplication, Destination, ExistingObjectReplication, ReplicationRule};
|
||||||
|
|
||||||
|
fn replication_rule(id: &str, arn: &str) -> ReplicationRule {
|
||||||
|
ReplicationRule {
|
||||||
|
delete_marker_replication: Some(DeleteMarkerReplication::default()),
|
||||||
|
delete_replication: None,
|
||||||
|
destination: Destination {
|
||||||
|
bucket: arn.to_string(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
existing_object_replication: Some(ExistingObjectReplication {
|
||||||
|
status: ExistingObjectReplicationStatus::from_static(ExistingObjectReplicationStatus::ENABLED),
|
||||||
|
}),
|
||||||
|
filter: None,
|
||||||
|
id: Some(id.to_string()),
|
||||||
|
prefix: Some(String::new()),
|
||||||
|
priority: Some(1),
|
||||||
|
source_selection_criteria: None,
|
||||||
|
status: ReplicationRuleStatus::from_static(ReplicationRuleStatus::ENABLED),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filter_target_arns_keeps_multiple_destinations_when_role_is_present() {
|
||||||
|
let config = ReplicationConfiguration {
|
||||||
|
role: "arn:legacy:target".to_string(),
|
||||||
|
rules: vec![
|
||||||
|
replication_rule("rule-1", "arn:target:a"),
|
||||||
|
replication_rule("rule-2", "arn:target:b"),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let arns = config.filter_target_arns(&ObjectOpts {
|
||||||
|
name: "object".to_string(),
|
||||||
|
op_type: ReplicationType::Object,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(arns.len(), 2);
|
||||||
|
assert!(arns.iter().any(|arn| arn == "arn:target:a"));
|
||||||
|
assert!(arns.iter().any(|arn| arn == "arn:target:b"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filter_target_arns_falls_back_to_role_when_destination_is_empty() {
|
||||||
|
let config = ReplicationConfiguration {
|
||||||
|
role: "arn:legacy:target".to_string(),
|
||||||
|
rules: vec![ReplicationRule {
|
||||||
|
delete_marker_replication: Some(DeleteMarkerReplication::default()),
|
||||||
|
delete_replication: None,
|
||||||
|
destination: Destination {
|
||||||
|
bucket: String::new(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
existing_object_replication: Some(ExistingObjectReplication {
|
||||||
|
status: ExistingObjectReplicationStatus::from_static(ExistingObjectReplicationStatus::ENABLED),
|
||||||
|
}),
|
||||||
|
filter: None,
|
||||||
|
id: Some("rule-1".to_string()),
|
||||||
|
prefix: Some(String::new()),
|
||||||
|
priority: Some(1),
|
||||||
|
source_selection_criteria: None,
|
||||||
|
status: ReplicationRuleStatus::from_static(ReplicationRuleStatus::ENABLED),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
let arns = config.filter_target_arns(&ObjectOpts {
|
||||||
|
name: "object".to_string(),
|
||||||
|
op_type: ReplicationType::Object,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(arns, vec!["arn:legacy:target".to_string()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -956,6 +956,9 @@ pub type DynReplicationPool = dyn ReplicationPoolTrait + Send + Sync;
|
|||||||
/// Trait that abstracts the replication pool operations
|
/// Trait that abstracts the replication pool operations
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait ReplicationPoolTrait: std::fmt::Debug {
|
pub trait ReplicationPoolTrait: std::fmt::Debug {
|
||||||
|
fn active_workers(&self) -> i32;
|
||||||
|
fn active_mrf_workers(&self) -> i32;
|
||||||
|
fn active_lrg_workers(&self) -> i32;
|
||||||
async fn queue_replica_task(&self, ri: ReplicateObjectInfo);
|
async fn queue_replica_task(&self, ri: ReplicateObjectInfo);
|
||||||
async fn queue_replica_delete_task(&self, ri: DeletedObjectReplicationInfo);
|
async fn queue_replica_delete_task(&self, ri: DeletedObjectReplicationInfo);
|
||||||
async fn resize(&self, priority: ReplicationPriority, max_workers: usize, max_l_workers: usize);
|
async fn resize(&self, priority: ReplicationPriority, max_workers: usize, max_l_workers: usize);
|
||||||
@@ -972,6 +975,18 @@ pub trait ReplicationPoolTrait: std::fmt::Debug {
|
|||||||
// Implement the trait for ReplicationPool
|
// Implement the trait for ReplicationPool
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl<S: StorageAPI> ReplicationPoolTrait for ReplicationPool<S> {
|
impl<S: StorageAPI> ReplicationPoolTrait for ReplicationPool<S> {
|
||||||
|
fn active_workers(&self) -> i32 {
|
||||||
|
ReplicationPool::<S>::active_workers(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn active_mrf_workers(&self) -> i32 {
|
||||||
|
ReplicationPool::<S>::active_mrf_workers(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn active_lrg_workers(&self) -> i32 {
|
||||||
|
ReplicationPool::<S>::active_lrg_workers(self)
|
||||||
|
}
|
||||||
|
|
||||||
async fn queue_replica_task(&self, ri: ReplicateObjectInfo) {
|
async fn queue_replica_task(&self, ri: ReplicateObjectInfo) {
|
||||||
self.queue_replica_task(ri).await;
|
self.queue_replica_task(ri).await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ use crate::set_disk::get_lock_acquire_timeout;
|
|||||||
use crate::store_api::{DeletedObject, HTTPRangeSpec, ObjectInfo, ObjectOptions, ObjectToDelete, WalkOptions};
|
use crate::store_api::{DeletedObject, HTTPRangeSpec, ObjectInfo, ObjectOptions, ObjectToDelete, WalkOptions};
|
||||||
use crate::{StorageAPI, new_object_layer_fn};
|
use crate::{StorageAPI, new_object_layer_fn};
|
||||||
use aws_sdk_s3::error::{ProvideErrorMetadata, SdkError};
|
use aws_sdk_s3::error::{ProvideErrorMetadata, SdkError};
|
||||||
use aws_sdk_s3::operation::head_object::HeadObjectOutput;
|
use aws_sdk_s3::operation::head_object::{HeadObjectError, HeadObjectOutput};
|
||||||
use aws_sdk_s3::primitives::ByteStream;
|
use aws_sdk_s3::primitives::ByteStream;
|
||||||
use aws_sdk_s3::types::{CompletedPart, ObjectLockLegalHoldStatus};
|
use aws_sdk_s3::types::{CompletedPart, ObjectLockLegalHoldStatus};
|
||||||
use aws_smithy_types::body::SdkBody;
|
use aws_smithy_types::body::SdkBody;
|
||||||
@@ -115,6 +115,41 @@ fn resync_state_accepts_update(state: &TargetReplicationResyncStatus, opts: &Res
|
|||||||
state.resync_id.is_empty() || opts.resync_id.is_empty() || state.resync_id == opts.resync_id
|
state.resync_id.is_empty() || opts.resync_id.is_empty() || state.resync_id == opts.resync_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn should_count_head_proxy_failure(is_not_found: bool, code: Option<&str>, raw_status: Option<u16>) -> bool {
|
||||||
|
if is_not_found || matches!(code, Some("MethodNotAllowed" | "405")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
!matches!(raw_status, Some(404 | 405))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_head_proxy_failure(err: &SdkError<HeadObjectError>) -> bool {
|
||||||
|
let (is_not_found, code) = err
|
||||||
|
.as_service_error()
|
||||||
|
.map(|service_err| (service_err.is_not_found(), service_err.code()))
|
||||||
|
.unwrap_or((false, None));
|
||||||
|
let raw_status = err.raw_response().map(|resp| resp.status().as_u16());
|
||||||
|
should_count_head_proxy_failure(is_not_found, code, raw_status)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn record_proxy_request(bucket: &str, api: &str, is_err: bool) {
|
||||||
|
if let Some(stats) = GLOBAL_REPLICATION_STATS.get() {
|
||||||
|
stats.inc_proxy(bucket, api, is_err).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn head_object_with_proxy_stats(
|
||||||
|
source_bucket: &str,
|
||||||
|
target_client: &TargetClient,
|
||||||
|
target_bucket: &str,
|
||||||
|
object: &str,
|
||||||
|
version_id: Option<String>,
|
||||||
|
) -> std::result::Result<HeadObjectOutput, SdkError<HeadObjectError>> {
|
||||||
|
let result = target_client.head_object(target_bucket, object, version_id).await;
|
||||||
|
let is_err = result.as_ref().err().is_some_and(is_head_proxy_failure);
|
||||||
|
record_proxy_request(source_bucket, "HeadObject", is_err).await;
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct ResyncOpts {
|
pub struct ResyncOpts {
|
||||||
pub bucket: String,
|
pub bucket: String,
|
||||||
@@ -748,10 +783,15 @@ impl ReplicationResyncer {
|
|||||||
|
|
||||||
let reset_id = target_client.reset_id.clone();
|
let reset_id = target_client.reset_id.clone();
|
||||||
|
|
||||||
let (size, err) = if let Err(err) = target_client
|
let head_result = head_object_with_proxy_stats(
|
||||||
.head_object(&target_client.bucket, &roi.name, roi.version_id.map(|v| v.to_string()))
|
&bucket_name,
|
||||||
.await
|
target_client.as_ref(),
|
||||||
{
|
&target_client.bucket,
|
||||||
|
&roi.name,
|
||||||
|
roi.version_id.map(|v| v.to_string()),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let (size, err) = if let Err(err) = head_result {
|
||||||
if roi.delete_marker {
|
if roi.delete_marker {
|
||||||
st.replicated_count += 1;
|
st.replicated_count += 1;
|
||||||
} else {
|
} else {
|
||||||
@@ -2120,9 +2160,14 @@ async fn replicate_delete_to_target(dobj: &DeletedObjectReplicationInfo, tgt_cli
|
|||||||
};
|
};
|
||||||
|
|
||||||
if dobj.delete_object.delete_marker && dobj.delete_object.delete_marker_version_id.is_some() {
|
if dobj.delete_object.delete_marker && dobj.delete_object.delete_marker_version_id.is_some() {
|
||||||
match tgt_client
|
match head_object_with_proxy_stats(
|
||||||
.head_object(&tgt_client.bucket, &dobj.delete_object.object_name, version_id.clone())
|
&dobj.bucket,
|
||||||
.await
|
tgt_client.as_ref(),
|
||||||
|
&tgt_client.bucket,
|
||||||
|
&dobj.delete_object.object_name,
|
||||||
|
version_id.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -2471,9 +2516,14 @@ impl ReplicateObjectInfoExt for ReplicateObjectInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut replication_action = replication_action;
|
let mut replication_action = replication_action;
|
||||||
match tgt_client
|
match head_object_with_proxy_stats(
|
||||||
.head_object(&tgt_client.bucket, &object, self.version_id.map(|v| v.to_string()))
|
&bucket,
|
||||||
.await
|
tgt_client.as_ref(),
|
||||||
|
&tgt_client.bucket,
|
||||||
|
&object,
|
||||||
|
self.version_id.map(|v| v.to_string()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
Ok(oi) => {
|
Ok(oi) => {
|
||||||
replication_action = get_replication_action(&object_info, &oi, self.op_type);
|
replication_action = get_replication_action(&object_info, &oi, self.op_type);
|
||||||
@@ -2528,9 +2578,10 @@ impl ReplicateObjectInfoExt for ReplicateObjectInfo {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let has_tagging_replication = !put_opts.user_tags.is_empty();
|
||||||
if let Some(err) = if is_multipart {
|
if let Some(err) = if is_multipart {
|
||||||
drop(gr);
|
drop(gr);
|
||||||
replicate_object_with_multipart(MultipartReplicationContext {
|
let result = replicate_object_with_multipart(MultipartReplicationContext {
|
||||||
storage: storage.clone(),
|
storage: storage.clone(),
|
||||||
cli: tgt_client.clone(),
|
cli: tgt_client.clone(),
|
||||||
src_bucket: &bucket,
|
src_bucket: &bucket,
|
||||||
@@ -2541,16 +2592,24 @@ impl ReplicateObjectInfoExt for ReplicateObjectInfo {
|
|||||||
arn: &rinfo.arn,
|
arn: &rinfo.arn,
|
||||||
put_opts,
|
put_opts,
|
||||||
})
|
})
|
||||||
.await
|
.await;
|
||||||
.err()
|
record_proxy_request(&bucket, "PutObject", result.is_err()).await;
|
||||||
|
if has_tagging_replication {
|
||||||
|
record_proxy_request(&bucket, "PutObjectTagging", result.is_err()).await;
|
||||||
|
}
|
||||||
|
result.err()
|
||||||
} else {
|
} else {
|
||||||
gr.stream = wrap_with_bandwidth_monitor(gr.stream, &put_opts, &bucket, &rinfo.arn);
|
gr.stream = wrap_with_bandwidth_monitor(gr.stream, &put_opts, &bucket, &rinfo.arn);
|
||||||
let byte_stream = async_read_to_bytestream(gr.stream);
|
let byte_stream = async_read_to_bytestream(gr.stream);
|
||||||
tgt_client
|
let result = tgt_client
|
||||||
.put_object(&tgt_client.bucket, &object, size, byte_stream, &put_opts)
|
.put_object(&tgt_client.bucket, &object, size, byte_stream, &put_opts)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| std::io::Error::other(e.to_string()))
|
.map_err(|e| std::io::Error::other(e.to_string()));
|
||||||
.err()
|
record_proxy_request(&bucket, "PutObject", result.is_err()).await;
|
||||||
|
if has_tagging_replication {
|
||||||
|
record_proxy_request(&bucket, "PutObjectTagging", result.is_err()).await;
|
||||||
|
}
|
||||||
|
result.err()
|
||||||
} {
|
} {
|
||||||
rinfo.replication_status = ReplicationStatusType::Failed;
|
rinfo.replication_status = ReplicationStatusType::Failed;
|
||||||
rinfo.error = Some(err.to_string());
|
rinfo.error = Some(err.to_string());
|
||||||
@@ -2690,9 +2749,14 @@ impl ReplicateObjectInfoExt for ReplicateObjectInfo {
|
|||||||
warn!("failed to set replication tagging directive header: {err}");
|
warn!("failed to set replication tagging directive header: {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
match tgt_client
|
match head_object_with_proxy_stats(
|
||||||
.head_object(&tgt_client.bucket, &object, self.version_id.map(|v| v.to_string()))
|
&bucket,
|
||||||
.await
|
tgt_client.as_ref(),
|
||||||
|
&tgt_client.bucket,
|
||||||
|
&object,
|
||||||
|
self.version_id.map(|v| v.to_string()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
Ok(oi) => {
|
Ok(oi) => {
|
||||||
replication_action = get_replication_action(&object_info, &oi, self.op_type);
|
replication_action = get_replication_action(&object_info, &oi, self.op_type);
|
||||||
@@ -2810,9 +2874,10 @@ impl ReplicateObjectInfoExt for ReplicateObjectInfo {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let has_tagging_replication = !put_opts.user_tags.is_empty();
|
||||||
if let Some(err) = if is_multipart {
|
if let Some(err) = if is_multipart {
|
||||||
drop(gr);
|
drop(gr);
|
||||||
replicate_object_with_multipart(MultipartReplicationContext {
|
let result = replicate_object_with_multipart(MultipartReplicationContext {
|
||||||
storage: storage.clone(),
|
storage: storage.clone(),
|
||||||
cli: tgt_client.clone(),
|
cli: tgt_client.clone(),
|
||||||
src_bucket: &bucket,
|
src_bucket: &bucket,
|
||||||
@@ -2823,16 +2888,24 @@ impl ReplicateObjectInfoExt for ReplicateObjectInfo {
|
|||||||
arn: &rinfo.arn,
|
arn: &rinfo.arn,
|
||||||
put_opts,
|
put_opts,
|
||||||
})
|
})
|
||||||
.await
|
.await;
|
||||||
.err()
|
record_proxy_request(&bucket, "PutObject", result.is_err()).await;
|
||||||
|
if has_tagging_replication {
|
||||||
|
record_proxy_request(&bucket, "PutObjectTagging", result.is_err()).await;
|
||||||
|
}
|
||||||
|
result.err()
|
||||||
} else {
|
} else {
|
||||||
gr.stream = wrap_with_bandwidth_monitor(gr.stream, &put_opts, &bucket, &rinfo.arn);
|
gr.stream = wrap_with_bandwidth_monitor(gr.stream, &put_opts, &bucket, &rinfo.arn);
|
||||||
let byte_stream = async_read_to_bytestream(gr.stream);
|
let byte_stream = async_read_to_bytestream(gr.stream);
|
||||||
tgt_client
|
let result = tgt_client
|
||||||
.put_object(&tgt_client.bucket, &object, size, byte_stream, &put_opts)
|
.put_object(&tgt_client.bucket, &object, size, byte_stream, &put_opts)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| std::io::Error::other(e.to_string()))
|
.map_err(|e| std::io::Error::other(e.to_string()));
|
||||||
.err()
|
record_proxy_request(&bucket, "PutObject", result.is_err()).await;
|
||||||
|
if has_tagging_replication {
|
||||||
|
record_proxy_request(&bucket, "PutObjectTagging", result.is_err()).await;
|
||||||
|
}
|
||||||
|
result.err()
|
||||||
} {
|
} {
|
||||||
rinfo.replication_status = ReplicationStatusType::Failed;
|
rinfo.replication_status = ReplicationStatusType::Failed;
|
||||||
rinfo.error = Some(err.to_string());
|
rinfo.error = Some(err.to_string());
|
||||||
@@ -3748,6 +3821,34 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_count_head_proxy_failure_ignores_not_found_and_405() {
|
||||||
|
assert!(
|
||||||
|
!should_count_head_proxy_failure(true, Some("NoSuchKey"), Some(404)),
|
||||||
|
"not-found heads are expected when the object has not reached the target yet"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!should_count_head_proxy_failure(false, Some("MethodNotAllowed"), Some(405)),
|
||||||
|
"405 delete-marker probing responses should not be counted as proxy failures"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!should_count_head_proxy_failure(false, Some("405"), Some(405)),
|
||||||
|
"numeric 405 codes must align with MethodNotAllowed semantics"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_should_count_head_proxy_failure_counts_unexpected_errors() {
|
||||||
|
assert!(
|
||||||
|
should_count_head_proxy_failure(false, Some("AccessDenied"), Some(403)),
|
||||||
|
"non-NotFound and non-405 service errors should be counted as failures"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
should_count_head_proxy_failure(false, None, Some(500)),
|
||||||
|
"raw 5xx head responses should be counted as proxy failures"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_get_heal_replicate_object_info_failed_object_returns_heal_roi() {
|
async fn test_get_heal_replicate_object_info_failed_object_returns_heal_roi() {
|
||||||
let oi = ObjectInfo {
|
let oi = ObjectInfo {
|
||||||
|
|||||||
@@ -12,18 +12,22 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
use crate::bucket::replication::get_global_replication_pool;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::global::get_global_bucket_monitor;
|
use crate::global::get_global_bucket_monitor;
|
||||||
use rustfs_filemeta::{ReplicatedTargetInfo, ReplicationStatusType, ReplicationType};
|
use rustfs_filemeta::{ReplicatedTargetInfo, ReplicationStatusType, ReplicationType};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicI64, Ordering};
|
use std::sync::atomic::{AtomicI64, Ordering};
|
||||||
use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
|
use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, Instant, SystemTime};
|
||||||
use tokio::sync::{Mutex, RwLock};
|
use tokio::sync::{Mutex, RwLock};
|
||||||
use tokio::time::interval;
|
use tokio::time::interval;
|
||||||
|
|
||||||
|
const ROLLING_WINDOW: Duration = Duration::from_secs(60);
|
||||||
|
const FAILURE_LAST_HOUR_WINDOW: Duration = Duration::from_secs(60 * 60);
|
||||||
|
|
||||||
/// Exponential Moving Average with thread-safe interior mutability
|
/// Exponential Moving Average with thread-safe interior mutability
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ExponentialMovingAverage {
|
pub struct ExponentialMovingAverage {
|
||||||
@@ -328,6 +332,13 @@ pub struct InQueueStats {
|
|||||||
pub now_count: AtomicI64,
|
pub now_count: AtomicI64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct QueueSample {
|
||||||
|
observed_at: Instant,
|
||||||
|
bytes: i64,
|
||||||
|
count: i64,
|
||||||
|
}
|
||||||
|
|
||||||
impl Clone for InQueueStats {
|
impl Clone for InQueueStats {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -359,9 +370,60 @@ pub struct InQueueMetric {
|
|||||||
pub curr: InQueueStats,
|
pub curr: InQueueStats,
|
||||||
pub avg: InQueueStats,
|
pub avg: InQueueStats,
|
||||||
pub max: InQueueStats,
|
pub max: InQueueStats,
|
||||||
|
pub last_minute: InQueueStats,
|
||||||
|
#[serde(skip)]
|
||||||
|
samples: VecDeque<QueueSample>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InQueueMetric {
|
impl InQueueMetric {
|
||||||
|
fn observe(&mut self, observed_at: Instant) {
|
||||||
|
let bytes = self.curr.now_bytes.load(Ordering::Relaxed);
|
||||||
|
let count = self.curr.now_count.load(Ordering::Relaxed);
|
||||||
|
|
||||||
|
self.curr.bytes = bytes;
|
||||||
|
self.curr.count = count;
|
||||||
|
self.samples.push_back(QueueSample {
|
||||||
|
observed_at,
|
||||||
|
bytes,
|
||||||
|
count,
|
||||||
|
});
|
||||||
|
|
||||||
|
while self
|
||||||
|
.samples
|
||||||
|
.front()
|
||||||
|
.is_some_and(|sample| observed_at.duration_since(sample.observed_at) > ROLLING_WINDOW)
|
||||||
|
{
|
||||||
|
self.samples.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.samples.is_empty() {
|
||||||
|
self.avg = InQueueStats::default();
|
||||||
|
self.max = InQueueStats::default();
|
||||||
|
self.last_minute = InQueueStats::default();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sample_count = self.samples.len() as i64;
|
||||||
|
let total_bytes = self.samples.iter().map(|sample| sample.bytes).sum::<i64>();
|
||||||
|
let total_count = self.samples.iter().map(|sample| sample.count).sum::<i64>();
|
||||||
|
let max_bytes = self.samples.iter().map(|sample| sample.bytes).max().unwrap_or(0);
|
||||||
|
let max_count = self.samples.iter().map(|sample| sample.count).max().unwrap_or(0);
|
||||||
|
|
||||||
|
self.avg.bytes = total_bytes / sample_count;
|
||||||
|
self.avg.count = total_count / sample_count;
|
||||||
|
self.max.bytes = max_bytes;
|
||||||
|
self.max.count = max_count;
|
||||||
|
self.last_minute.bytes = self.avg.bytes;
|
||||||
|
self.last_minute.count = self.avg.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot(&self) -> Self {
|
||||||
|
let mut snapshot = self.clone();
|
||||||
|
snapshot.curr.bytes = snapshot.curr.now_bytes.load(Ordering::Relaxed);
|
||||||
|
snapshot.curr.count = snapshot.curr.now_count.load(Ordering::Relaxed);
|
||||||
|
snapshot
|
||||||
|
}
|
||||||
|
|
||||||
pub fn merge(&self, other: &InQueueMetric) -> Self {
|
pub fn merge(&self, other: &InQueueMetric) -> Self {
|
||||||
Self {
|
Self {
|
||||||
curr: InQueueStats {
|
curr: InQueueStats {
|
||||||
@@ -384,6 +446,12 @@ impl InQueueMetric {
|
|||||||
count: self.max.count.max(other.max.count),
|
count: self.max.count.max(other.max.count),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
last_minute: InQueueStats {
|
||||||
|
bytes: self.last_minute.bytes + other.last_minute.bytes,
|
||||||
|
count: self.last_minute.count + other.last_minute.count,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
samples: VecDeque::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -391,8 +459,8 @@ impl InQueueMetric {
|
|||||||
/// Queue cache
|
/// Queue cache
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct QueueCache {
|
pub struct QueueCache {
|
||||||
pub bucket_stats: HashMap<String, InQueueStats>,
|
pub bucket_stats: HashMap<String, InQueueMetric>,
|
||||||
pub sr_queue_stats: InQueueStats,
|
pub sr_queue_stats: InQueueMetric,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QueueCache {
|
impl QueueCache {
|
||||||
@@ -401,36 +469,19 @@ impl QueueCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self) {
|
pub fn update(&mut self) {
|
||||||
// Update queue statistics cache
|
let observed_at = Instant::now();
|
||||||
// In actual implementation, this would get latest statistics from queue system
|
self.sr_queue_stats.observe(observed_at);
|
||||||
|
for stats in self.bucket_stats.values_mut() {
|
||||||
|
stats.observe(observed_at);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_bucket_stats(&self, bucket: &str) -> InQueueMetric {
|
pub fn get_bucket_stats(&self, bucket: &str) -> InQueueMetric {
|
||||||
if let Some(bucket_stat) = self.bucket_stats.get(bucket) {
|
self.bucket_stats.get(bucket).map(InQueueMetric::snapshot).unwrap_or_default()
|
||||||
InQueueMetric {
|
|
||||||
curr: InQueueStats {
|
|
||||||
bytes: bucket_stat.now_bytes.load(Ordering::Relaxed),
|
|
||||||
count: bucket_stat.now_count.load(Ordering::Relaxed),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
avg: InQueueStats::default(), // simplified implementation
|
|
||||||
max: InQueueStats::default(), // simplified implementation
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
InQueueMetric::default()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_site_stats(&self) -> InQueueMetric {
|
pub fn get_site_stats(&self) -> InQueueMetric {
|
||||||
InQueueMetric {
|
self.sr_queue_stats.snapshot()
|
||||||
curr: InQueueStats {
|
|
||||||
bytes: self.sr_queue_stats.now_bytes.load(Ordering::Relaxed),
|
|
||||||
count: self.sr_queue_stats.now_count.load(Ordering::Relaxed),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
avg: InQueueStats::default(), // simplified implementation
|
|
||||||
max: InQueueStats::default(), // simplified implementation
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,8 +489,14 @@ impl QueueCache {
|
|||||||
pub struct ProxyMetric {
|
pub struct ProxyMetric {
|
||||||
pub get_total: i64,
|
pub get_total: i64,
|
||||||
pub get_failed: i64,
|
pub get_failed: i64,
|
||||||
|
pub get_tag_total: i64,
|
||||||
|
pub get_tag_failed: i64,
|
||||||
pub put_total: i64,
|
pub put_total: i64,
|
||||||
pub put_failed: i64,
|
pub put_failed: i64,
|
||||||
|
pub put_tag_total: i64,
|
||||||
|
pub put_tag_failed: i64,
|
||||||
|
pub delete_tag_total: i64,
|
||||||
|
pub delete_tag_failed: i64,
|
||||||
pub head_total: i64,
|
pub head_total: i64,
|
||||||
pub head_failed: i64,
|
pub head_failed: i64,
|
||||||
}
|
}
|
||||||
@@ -448,8 +505,14 @@ impl ProxyMetric {
|
|||||||
pub fn add(&mut self, other: &ProxyMetric) {
|
pub fn add(&mut self, other: &ProxyMetric) {
|
||||||
self.get_total += other.get_total;
|
self.get_total += other.get_total;
|
||||||
self.get_failed += other.get_failed;
|
self.get_failed += other.get_failed;
|
||||||
|
self.get_tag_total += other.get_tag_total;
|
||||||
|
self.get_tag_failed += other.get_tag_failed;
|
||||||
self.put_total += other.put_total;
|
self.put_total += other.put_total;
|
||||||
self.put_failed += other.put_failed;
|
self.put_failed += other.put_failed;
|
||||||
|
self.put_tag_total += other.put_tag_total;
|
||||||
|
self.put_tag_failed += other.put_tag_failed;
|
||||||
|
self.delete_tag_total += other.delete_tag_total;
|
||||||
|
self.delete_tag_failed += other.delete_tag_failed;
|
||||||
self.head_total += other.head_total;
|
self.head_total += other.head_total;
|
||||||
self.head_failed += other.head_failed;
|
self.head_failed += other.head_failed;
|
||||||
}
|
}
|
||||||
@@ -476,18 +539,36 @@ impl ProxyStatsCache {
|
|||||||
metric.get_failed += 1;
|
metric.get_failed += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"GetObjectTagging" => {
|
||||||
|
metric.get_tag_total += 1;
|
||||||
|
if is_err {
|
||||||
|
metric.get_tag_failed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
"PutObject" => {
|
"PutObject" => {
|
||||||
metric.put_total += 1;
|
metric.put_total += 1;
|
||||||
if is_err {
|
if is_err {
|
||||||
metric.put_failed += 1;
|
metric.put_failed += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"PutObjectTagging" => {
|
||||||
|
metric.put_tag_total += 1;
|
||||||
|
if is_err {
|
||||||
|
metric.put_tag_failed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
"HeadObject" => {
|
"HeadObject" => {
|
||||||
metric.head_total += 1;
|
metric.head_total += 1;
|
||||||
if is_err {
|
if is_err {
|
||||||
metric.head_failed += 1;
|
metric.head_failed += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"DeleteObjectTagging" => {
|
||||||
|
metric.delete_tag_total += 1;
|
||||||
|
if is_err {
|
||||||
|
metric.delete_tag_failed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -505,11 +586,19 @@ impl ProxyStatsCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct FailureSample {
|
||||||
|
observed_at: Instant,
|
||||||
|
size: i64,
|
||||||
|
}
|
||||||
|
|
||||||
/// Failure statistics
|
/// Failure statistics
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
pub struct FailStats {
|
pub struct FailStats {
|
||||||
pub count: i64,
|
pub count: i64,
|
||||||
pub size: i64,
|
pub size: i64,
|
||||||
|
#[serde(skip)]
|
||||||
|
recent: VecDeque<FailureSample>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FailStats {
|
impl FailStats {
|
||||||
@@ -518,14 +607,42 @@ impl FailStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_size(&mut self, size: i64, _err: Option<&Error>) {
|
pub fn add_size(&mut self, size: i64, _err: Option<&Error>) {
|
||||||
|
let observed_at = Instant::now();
|
||||||
self.count += 1;
|
self.count += 1;
|
||||||
self.size += size;
|
self.size += size;
|
||||||
|
self.recent.push_back(FailureSample { observed_at, size });
|
||||||
|
self.prune(observed_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prune(&mut self, observed_at: Instant) {
|
||||||
|
while self
|
||||||
|
.recent
|
||||||
|
.front()
|
||||||
|
.is_some_and(|sample| observed_at.duration_since(sample.observed_at) > FAILURE_LAST_HOUR_WINDOW)
|
||||||
|
{
|
||||||
|
self.recent.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recent_since(&self, window: Duration) -> FailedMetric {
|
||||||
|
let now = Instant::now();
|
||||||
|
let mut count = 0i64;
|
||||||
|
let mut size = 0i64;
|
||||||
|
for sample in self.recent.iter().rev() {
|
||||||
|
if now.duration_since(sample.observed_at) > window {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
count += 1;
|
||||||
|
size += sample.size;
|
||||||
|
}
|
||||||
|
FailedMetric { count, size }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn merge(&self, other: &FailStats) -> Self {
|
pub fn merge(&self, other: &FailStats) -> Self {
|
||||||
Self {
|
Self {
|
||||||
count: self.count + other.count,
|
count: self.count + other.count,
|
||||||
size: self.size + other.size,
|
size: self.size + other.size,
|
||||||
|
recent: VecDeque::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,6 +791,14 @@ pub struct ActiveWorkerStat {
|
|||||||
pub curr: i32,
|
pub curr: i32,
|
||||||
pub max: i32,
|
pub max: i32,
|
||||||
pub avg: f64,
|
pub avg: f64,
|
||||||
|
#[serde(skip)]
|
||||||
|
samples: VecDeque<WorkerSample>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct WorkerSample {
|
||||||
|
observed_at: Instant,
|
||||||
|
workers: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActiveWorkerStat {
|
impl ActiveWorkerStat {
|
||||||
@@ -685,9 +810,31 @@ impl ActiveWorkerStat {
|
|||||||
self.clone()
|
self.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self) {
|
pub fn update(&mut self, curr: i32) {
|
||||||
// Simulate worker statistics update logic
|
let observed_at = Instant::now();
|
||||||
// In actual implementation, this would get current active count from worker pool
|
self.curr = curr;
|
||||||
|
self.samples.push_back(WorkerSample {
|
||||||
|
observed_at,
|
||||||
|
workers: curr,
|
||||||
|
});
|
||||||
|
|
||||||
|
while self
|
||||||
|
.samples
|
||||||
|
.front()
|
||||||
|
.is_some_and(|sample| observed_at.duration_since(sample.observed_at) > ROLLING_WINDOW)
|
||||||
|
{
|
||||||
|
self.samples.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.samples.is_empty() {
|
||||||
|
self.max = curr;
|
||||||
|
self.avg = curr as f64;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.max = self.samples.iter().map(|sample| sample.workers).max().unwrap_or(curr);
|
||||||
|
let total = self.samples.iter().map(|sample| sample.workers as i64).sum::<i64>();
|
||||||
|
self.avg = total as f64 / self.samples.len() as f64;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -740,8 +887,11 @@ impl ReplicationStats {
|
|||||||
let mut interval = interval(Duration::from_secs(2));
|
let mut interval = interval(Duration::from_secs(2));
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
|
let current = get_global_replication_pool()
|
||||||
|
.map(|pool| pool.active_workers() + pool.active_lrg_workers() + pool.active_mrf_workers())
|
||||||
|
.unwrap_or(0);
|
||||||
let mut workers = workers_clone.lock().await;
|
let mut workers = workers_clone.lock().await;
|
||||||
workers.update();
|
workers.update(current);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -925,14 +1075,26 @@ impl ReplicationStats {
|
|||||||
/// Get replication metrics for all buckets
|
/// Get replication metrics for all buckets
|
||||||
pub async fn get_all(&self) -> HashMap<String, BucketReplicationStats> {
|
pub async fn get_all(&self) -> HashMap<String, BucketReplicationStats> {
|
||||||
let cache = self.cache.read().await;
|
let cache = self.cache.read().await;
|
||||||
let mut result = HashMap::new();
|
let mut result = HashMap::with_capacity(cache.len());
|
||||||
|
|
||||||
for (bucket, stats) in cache.iter() {
|
for (bucket, stats) in cache.iter() {
|
||||||
let mut cloned_stats = stats.clone_stats();
|
result.insert(bucket.clone(), stats.clone_stats());
|
||||||
// Add queue statistics
|
}
|
||||||
|
drop(cache);
|
||||||
|
|
||||||
|
{
|
||||||
let q_cache = self.q_cache.lock().await;
|
let q_cache = self.q_cache.lock().await;
|
||||||
cloned_stats.q_stat = q_cache.get_bucket_stats(bucket);
|
for (bucket, queue_stats) in &q_cache.bucket_stats {
|
||||||
result.insert(bucket.clone(), cloned_stats);
|
let bucket_stats = result.entry(bucket.clone()).or_insert_with(BucketReplicationStats::new);
|
||||||
|
bucket_stats.q_stat = queue_stats.snapshot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let p_cache = self.p_cache.lock().await;
|
||||||
|
for bucket in p_cache.bucket_stats.keys() {
|
||||||
|
result.entry(bucket.clone()).or_insert_with(BucketReplicationStats::new);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
@@ -1114,12 +1276,12 @@ impl ReplicationStats {
|
|||||||
let stats = q_cache
|
let stats = q_cache
|
||||||
.bucket_stats
|
.bucket_stats
|
||||||
.entry(bucket.to_string())
|
.entry(bucket.to_string())
|
||||||
.or_insert_with(InQueueStats::default);
|
.or_insert_with(InQueueMetric::default);
|
||||||
stats.now_bytes.fetch_add(size, Ordering::Relaxed);
|
stats.curr.now_bytes.fetch_add(size, Ordering::Relaxed);
|
||||||
stats.now_count.fetch_add(1, Ordering::Relaxed);
|
stats.curr.now_count.fetch_add(1, Ordering::Relaxed);
|
||||||
|
|
||||||
q_cache.sr_queue_stats.now_bytes.fetch_add(size, Ordering::Relaxed);
|
q_cache.sr_queue_stats.curr.now_bytes.fetch_add(size, Ordering::Relaxed);
|
||||||
q_cache.sr_queue_stats.now_count.fetch_add(1, Ordering::Relaxed);
|
q_cache.sr_queue_stats.curr.now_count.fetch_add(1, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decrease queue statistics
|
/// Decrease queue statistics
|
||||||
@@ -1128,12 +1290,12 @@ impl ReplicationStats {
|
|||||||
let stats = q_cache
|
let stats = q_cache
|
||||||
.bucket_stats
|
.bucket_stats
|
||||||
.entry(bucket.to_string())
|
.entry(bucket.to_string())
|
||||||
.or_insert_with(InQueueStats::default);
|
.or_insert_with(InQueueMetric::default);
|
||||||
stats.now_bytes.fetch_sub(size, Ordering::Relaxed);
|
stats.curr.now_bytes.fetch_sub(size, Ordering::Relaxed);
|
||||||
stats.now_count.fetch_sub(1, Ordering::Relaxed);
|
stats.curr.now_count.fetch_sub(1, Ordering::Relaxed);
|
||||||
|
|
||||||
q_cache.sr_queue_stats.now_bytes.fetch_sub(size, Ordering::Relaxed);
|
q_cache.sr_queue_stats.curr.now_bytes.fetch_sub(size, Ordering::Relaxed);
|
||||||
q_cache.sr_queue_stats.now_count.fetch_sub(1, Ordering::Relaxed);
|
q_cache.sr_queue_stats.curr.now_count.fetch_sub(1, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Increase proxy metrics
|
/// Increase proxy metrics
|
||||||
@@ -1166,6 +1328,51 @@ mod tests {
|
|||||||
assert_eq!(workers.curr, 0);
|
assert_eq!(workers.curr, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_in_queue_metric_observe_updates_rolling_stats() {
|
||||||
|
let mut metric = InQueueMetric::default();
|
||||||
|
metric.curr.now_bytes.store(128, Ordering::Relaxed);
|
||||||
|
metric.curr.now_count.store(4, Ordering::Relaxed);
|
||||||
|
metric.observe(Instant::now());
|
||||||
|
|
||||||
|
metric.curr.now_bytes.store(256, Ordering::Relaxed);
|
||||||
|
metric.curr.now_count.store(6, Ordering::Relaxed);
|
||||||
|
metric.observe(Instant::now());
|
||||||
|
|
||||||
|
assert_eq!(metric.curr.bytes, 256);
|
||||||
|
assert_eq!(metric.curr.count, 6);
|
||||||
|
assert_eq!(metric.max.bytes, 256);
|
||||||
|
assert_eq!(metric.max.count, 6);
|
||||||
|
assert_eq!(metric.last_minute.bytes, 192);
|
||||||
|
assert_eq!(metric.last_minute.count, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fail_stats_recent_since_tracks_windows() {
|
||||||
|
let mut stats = FailStats::default();
|
||||||
|
stats.add_size(64, None);
|
||||||
|
stats.add_size(32, None);
|
||||||
|
|
||||||
|
let last_minute = stats.recent_since(Duration::from_secs(60));
|
||||||
|
let last_hour = stats.recent_since(Duration::from_secs(60 * 60));
|
||||||
|
assert_eq!(last_minute.count, 2);
|
||||||
|
assert_eq!(last_minute.size, 96);
|
||||||
|
assert_eq!(last_hour.count, 2);
|
||||||
|
assert_eq!(last_hour.size, 96);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_active_worker_stat_update_tracks_rolling_avg_and_max() {
|
||||||
|
let mut stats = ActiveWorkerStat::default();
|
||||||
|
stats.update(2);
|
||||||
|
stats.update(6);
|
||||||
|
stats.update(4);
|
||||||
|
|
||||||
|
assert_eq!(stats.curr, 4);
|
||||||
|
assert_eq!(stats.max, 6);
|
||||||
|
assert_eq!(stats.avg, 4.0);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_delete_bucket_stats() {
|
async fn test_delete_bucket_stats() {
|
||||||
let stats = ReplicationStats::new();
|
let stats = ReplicationStats::new();
|
||||||
@@ -1218,6 +1425,15 @@ mod tests {
|
|||||||
assert_eq!(stat.replicated_count, 1);
|
assert_eq!(stat.replicated_count, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_get_all_includes_proxy_only_bucket() {
|
||||||
|
let stats = ReplicationStats::new();
|
||||||
|
stats.inc_proxy("proxy-only-bucket", "HeadObject", false).await;
|
||||||
|
|
||||||
|
let all = stats.get_all().await;
|
||||||
|
assert!(all.contains_key("proxy-only-bucket"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sr_stats() {
|
fn test_sr_stats() {
|
||||||
let sr_stats = SRStats::new();
|
let sr_stats = SRStats::new();
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use super::constants::UNSIGNED_PAYLOAD;
|
|||||||
use super::credentials::SignatureType;
|
use super::credentials::SignatureType;
|
||||||
use crate::client::{
|
use crate::client::{
|
||||||
api_error_response::http_resp_to_error_response,
|
api_error_response::http_resp_to_error_response,
|
||||||
|
signer_error,
|
||||||
transition_api::{CreateBucketConfiguration, LocationConstraint, TransitionClient},
|
transition_api::{CreateBucketConfiguration, LocationConstraint, TransitionClient},
|
||||||
};
|
};
|
||||||
use http::Request;
|
use http::Request;
|
||||||
@@ -35,6 +36,10 @@ use rustfs_utils::hash::EMPTY_STRING_SHA256_HASH;
|
|||||||
use s3s::S3ErrorCode;
|
use s3s::S3ErrorCode;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
fn signer_error_to_io_error(scope: &str, error: rustfs_signer::SignV4Error) -> std::io::Error {
|
||||||
|
signer_error::signer_error_to_io_error(scope, error)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct BucketLocationCache {
|
pub struct BucketLocationCache {
|
||||||
items: HashMap<String, String>,
|
items: HashMap<String, String>,
|
||||||
@@ -179,10 +184,15 @@ impl TransitionClient {
|
|||||||
content_sha256 = UNSIGNED_PAYLOAD.to_string();
|
content_sha256 = UNSIGNED_PAYLOAD.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(content_sha256_value) = content_sha256.parse() {
|
let content_sha256_value = content_sha256.parse().map_err(|err| {
|
||||||
req.headers_mut().insert("X-Amz-Content-Sha256", content_sha256_value);
|
std::io::Error::new(
|
||||||
}
|
std::io::ErrorKind::InvalidInput,
|
||||||
let req = rustfs_signer::sign_v4(req, 0, &access_key_id, &secret_access_key, &session_token, "us-east-1");
|
format!("invalid X-Amz-Content-Sha256 header value: {err}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
req.headers_mut().insert("X-Amz-Content-Sha256", content_sha256_value);
|
||||||
|
let req = rustfs_signer::try_sign_v4(req, 0, &access_key_id, &secret_access_key, &session_token, "us-east-1")
|
||||||
|
.map_err(|err| signer_error_to_io_error("failed to sign bucket location request", err))?;
|
||||||
Ok(req)
|
Ok(req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,5 +35,6 @@ pub mod constants;
|
|||||||
pub mod credentials;
|
pub mod credentials;
|
||||||
pub mod object_api_utils;
|
pub mod object_api_utils;
|
||||||
pub mod object_handlers_common;
|
pub mod object_handlers_common;
|
||||||
|
pub mod signer_error;
|
||||||
pub mod transition_api;
|
pub mod transition_api;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use std::error::Error as StdError;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
use std::io::{Error, ErrorKind};
|
||||||
|
|
||||||
|
pub(crate) const SIGNER_HEADER_ERROR_MARKER: &str = "rustfs_signer_header_error";
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct SignerHeaderError {
|
||||||
|
scope: String,
|
||||||
|
header_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SignerHeaderError {
|
||||||
|
fn new(scope: &str, header_name: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
scope: scope.to_string(),
|
||||||
|
header_name: header_name.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for SignerHeaderError {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}: invalid UTF-8 header value for `{}` [{}]",
|
||||||
|
self.scope, self.header_name, SIGNER_HEADER_ERROR_MARKER
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StdError for SignerHeaderError {}
|
||||||
|
|
||||||
|
pub(crate) fn invalid_utf8_header_error(scope: &str, header_name: &str) -> Error {
|
||||||
|
Error::new(ErrorKind::InvalidInput, SignerHeaderError::new(scope, header_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn signer_error_to_io_error(scope: &str, error: rustfs_signer::SignV4Error) -> Error {
|
||||||
|
match error {
|
||||||
|
rustfs_signer::SignV4Error::InvalidHeaderValue { name } => invalid_utf8_header_error(scope, &name),
|
||||||
|
other => Error::other(format!("{scope}: {other}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn error_chain_contains_signer_header_marker(err: &(dyn StdError + 'static)) -> bool {
|
||||||
|
let mut current = Some(err);
|
||||||
|
while let Some(source) = current {
|
||||||
|
if source.downcast_ref::<SignerHeaderError>().is_some() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if source.to_string().contains(SIGNER_HEADER_ERROR_MARKER) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = source.source();
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{error_chain_contains_signer_header_marker, invalid_utf8_header_error, signer_error_to_io_error};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_utf8_header_error_is_detected_through_error_chain() {
|
||||||
|
let err = invalid_utf8_header_error("failed to sign request", "x-amz-meta-invalid");
|
||||||
|
|
||||||
|
assert!(error_chain_contains_signer_header_marker(&err));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mapped_signer_header_error_is_detected_through_error_chain() {
|
||||||
|
let err = signer_error_to_io_error(
|
||||||
|
"failed to sign request",
|
||||||
|
rustfs_signer::SignV4Error::InvalidHeaderValue {
|
||||||
|
name: "x-amz-meta-invalid".to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(error_chain_contains_signer_header_marker(&err));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generic_io_errors_do_not_match_signer_header_marker() {
|
||||||
|
let err = std::io::Error::other("unrelated failure");
|
||||||
|
|
||||||
|
assert!(!error_chain_contains_signer_header_marker(&err));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ use crate::client::{
|
|||||||
},
|
},
|
||||||
constants::{UNSIGNED_PAYLOAD, UNSIGNED_PAYLOAD_TRAILER},
|
constants::{UNSIGNED_PAYLOAD, UNSIGNED_PAYLOAD_TRAILER},
|
||||||
credentials::{CredContext, Credentials, SignatureType, Static},
|
credentials::{CredContext, Credentials, SignatureType, Static},
|
||||||
|
signer_error,
|
||||||
};
|
};
|
||||||
use crate::{client::checksum::ChecksumMode, store_api::GetObjectReader};
|
use crate::{client::checksum::ChecksumMode, store_api::GetObjectReader};
|
||||||
use futures::{Future, StreamExt};
|
use futures::{Future, StreamExt};
|
||||||
@@ -85,6 +86,21 @@ const C_UNKNOWN: i32 = -1;
|
|||||||
const C_OFFLINE: i32 = 0;
|
const C_OFFLINE: i32 = 0;
|
||||||
const C_ONLINE: i32 = 1;
|
const C_ONLINE: i32 = 1;
|
||||||
|
|
||||||
|
fn invalid_utf8_header_error(scope: &str, header_name: &str) -> std::io::Error {
|
||||||
|
signer_error::invalid_utf8_header_error(scope, header_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_header_values(headers: &HeaderMap, scope: &str) -> Result<(), std::io::Error> {
|
||||||
|
for (name, value) in headers {
|
||||||
|
value.to_str().map_err(|_| invalid_utf8_header_error(scope, name.as_str()))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signer_error_to_io_error(scope: &str, error: rustfs_signer::SignV4Error) -> std::io::Error {
|
||||||
|
signer_error::signer_error_to_io_error(scope, error)
|
||||||
|
}
|
||||||
|
|
||||||
//pub type ReaderImpl = Box<dyn Reader + Send + Sync + 'static>;
|
//pub type ReaderImpl = Box<dyn Reader + Send + Sync + 'static>;
|
||||||
pub enum ReaderImpl {
|
pub enum ReaderImpl {
|
||||||
Body(Bytes),
|
Body(Bytes),
|
||||||
@@ -560,8 +576,9 @@ impl TransitionClient {
|
|||||||
"extra signed headers for presign with signature v2 is not supported.",
|
"extra signed headers for presign with signature v2 is not supported.",
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let headers = req.headers_mut();
|
|
||||||
if let Some(extra_headers) = metadata.extra_pre_sign_header.as_ref() {
|
if let Some(extra_headers) = metadata.extra_pre_sign_header.as_ref() {
|
||||||
|
validate_header_values(extra_headers, "presign extra header")?;
|
||||||
|
let headers = req.headers_mut();
|
||||||
for (k, v) in extra_headers {
|
for (k, v) in extra_headers {
|
||||||
headers.insert(k, v.clone());
|
headers.insert(k, v.clone());
|
||||||
}
|
}
|
||||||
@@ -570,7 +587,7 @@ impl TransitionClient {
|
|||||||
if signer_type == SignatureType::SignatureV2 {
|
if signer_type == SignatureType::SignatureV2 {
|
||||||
req = rustfs_signer::pre_sign_v2(req, &access_key_id, &secret_access_key, metadata.expires, is_virtual_host);
|
req = rustfs_signer::pre_sign_v2(req, &access_key_id, &secret_access_key, metadata.expires, is_virtual_host);
|
||||||
} else if signer_type == SignatureType::SignatureV4 {
|
} else if signer_type == SignatureType::SignatureV4 {
|
||||||
req = rustfs_signer::pre_sign_v4(
|
req = rustfs_signer::try_pre_sign_v4(
|
||||||
req,
|
req,
|
||||||
&access_key_id,
|
&access_key_id,
|
||||||
&secret_access_key,
|
&secret_access_key,
|
||||||
@@ -578,12 +595,14 @@ impl TransitionClient {
|
|||||||
&location,
|
&location,
|
||||||
metadata.expires,
|
metadata.expires,
|
||||||
OffsetDateTime::now_utc(),
|
OffsetDateTime::now_utc(),
|
||||||
);
|
)
|
||||||
|
.map_err(|err| signer_error_to_io_error("failed to presign v4 request", err))?;
|
||||||
}
|
}
|
||||||
return Ok(req);
|
return Ok(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.set_user_agent(&mut req);
|
self.set_user_agent(&mut req);
|
||||||
|
validate_header_values(&metadata.custom_header, "request custom header")?;
|
||||||
|
|
||||||
for (k, v) in metadata.custom_header.clone() {
|
for (k, v) in metadata.custom_header.clone() {
|
||||||
if let Some(key) = k {
|
if let Some(key) = k {
|
||||||
@@ -593,15 +612,15 @@ impl TransitionClient {
|
|||||||
|
|
||||||
//req.content_length = metadata.content_length;
|
//req.content_length = metadata.content_length;
|
||||||
if metadata.content_length <= -1 {
|
if metadata.content_length <= -1 {
|
||||||
if let Ok(chunked_value) = HeaderValue::from_str(&vec!["chunked"].join(",")) {
|
req.headers_mut()
|
||||||
req.headers_mut().insert(http::header::TRANSFER_ENCODING, chunked_value);
|
.insert(http::header::TRANSFER_ENCODING, HeaderValue::from_static("chunked"));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if metadata.content_md5_base64.len() > 0 {
|
if !metadata.content_md5_base64.is_empty() {
|
||||||
if let Ok(md5_value) = HeaderValue::from_str(&metadata.content_md5_base64) {
|
let md5_value = HeaderValue::from_str(&metadata.content_md5_base64).map_err(|err| {
|
||||||
req.headers_mut().insert("Content-Md5", md5_value);
|
std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("invalid Content-Md5 header value: {err}"))
|
||||||
}
|
})?;
|
||||||
|
req.headers_mut().insert("Content-Md5", md5_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if signer_type == SignatureType::SignatureAnonymous {
|
if signer_type == SignatureType::SignatureAnonymous {
|
||||||
@@ -634,14 +653,15 @@ impl TransitionClient {
|
|||||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
|
||||||
req.headers_mut().insert(header_name, header_value);
|
req.headers_mut().insert(header_name, header_value);
|
||||||
|
|
||||||
req = rustfs_signer::sign_v4_trailer(
|
req = rustfs_signer::try_sign_v4_trailer(
|
||||||
req,
|
req,
|
||||||
&access_key_id,
|
&access_key_id,
|
||||||
&secret_access_key,
|
&secret_access_key,
|
||||||
&session_token,
|
&session_token,
|
||||||
&location,
|
&location,
|
||||||
metadata.trailer.clone(),
|
metadata.trailer.clone(),
|
||||||
);
|
)
|
||||||
|
.map_err(|err| signer_error_to_io_error("failed to sign v4 request", err))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if metadata.content_length > 0 {
|
if metadata.content_length > 0 {
|
||||||
@@ -1354,7 +1374,10 @@ pub struct CreateBucketConfiguration {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{build_tls_config, load_root_store_from_tls_path, with_rustls_init_guard};
|
use super::{
|
||||||
|
build_tls_config, load_root_store_from_tls_path, signer_error_to_io_error, validate_header_values, with_rustls_init_guard,
|
||||||
|
};
|
||||||
|
use http::{HeaderMap, HeaderValue};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rustls_guard_converts_panics_to_io_errors() {
|
fn rustls_guard_converts_panics_to_io_errors() {
|
||||||
@@ -1404,4 +1427,29 @@ mod tests {
|
|||||||
});
|
});
|
||||||
assert!(outcome.is_ok(), "provider install guard must not panic when a provider is already set");
|
assert!(outcome.is_ok(), "provider install guard must not panic when a provider is already set");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_header_values_returns_header_name_for_non_utf8_values() {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert(
|
||||||
|
"x-amz-meta-invalid",
|
||||||
|
HeaderValue::from_bytes(&[0xFF]).expect("invalid utf8 bytes should be accepted by HeaderValue"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let err =
|
||||||
|
validate_header_values(&headers, "request custom header").expect_err("invalid header value should fail validation");
|
||||||
|
assert!(err.to_string().contains("x-amz-meta-invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn signer_error_mapping_preserves_header_name() {
|
||||||
|
let err = signer_error_to_io_error(
|
||||||
|
"failed to sign v4 request",
|
||||||
|
rustfs_signer::SignV4Error::InvalidHeaderValue {
|
||||||
|
name: "x-amz-meta-invalid".to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||||
|
assert!(err.to_string().contains("x-amz-meta-invalid"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,15 +14,16 @@
|
|||||||
|
|
||||||
use crate::config::{KV, KVS};
|
use crate::config::{KV, KVS};
|
||||||
use rustfs_config::{
|
use rustfs_config::{
|
||||||
COMMENT_KEY, DEFAULT_LIMIT, ENABLE_KEY, EVENT_DEFAULT_DIR, EnableState, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD,
|
COMMENT_KEY, DEFAULT_LIMIT, ENABLE_KEY, EVENT_DEFAULT_DIR, EnableState, KAFKA_ACKS, KAFKA_BROKERS, KAFKA_QUEUE_DIR,
|
||||||
MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TLS_CA, MQTT_TLS_CLIENT_CERT, MQTT_TLS_CLIENT_KEY,
|
KAFKA_QUEUE_LIMIT, KAFKA_TLS_CA, KAFKA_TLS_CLIENT_CERT, KAFKA_TLS_CLIENT_KEY, KAFKA_TLS_ENABLE, KAFKA_TOPIC, MQTT_BROKER,
|
||||||
MQTT_TLS_POLICY, MQTT_TLS_TRUST_LEAF_AS_CA, MQTT_TOPIC, MQTT_USERNAME, MQTT_WS_PATH_ALLOWLIST, NATS_ADDRESS,
|
MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD, MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TLS_CA,
|
||||||
NATS_CREDENTIALS_FILE, NATS_PASSWORD, NATS_QUEUE_DIR, NATS_QUEUE_LIMIT, NATS_SUBJECT, NATS_TLS_CA, NATS_TLS_CLIENT_CERT,
|
MQTT_TLS_CLIENT_CERT, MQTT_TLS_CLIENT_KEY, MQTT_TLS_POLICY, MQTT_TLS_TRUST_LEAF_AS_CA, MQTT_TOPIC, MQTT_USERNAME,
|
||||||
NATS_TLS_CLIENT_KEY, NATS_TLS_REQUIRED, NATS_TOKEN, NATS_USERNAME, PULSAR_AUTH_TOKEN, PULSAR_BROKER, PULSAR_PASSWORD,
|
MQTT_WS_PATH_ALLOWLIST, NATS_ADDRESS, NATS_CREDENTIALS_FILE, NATS_PASSWORD, NATS_QUEUE_DIR, NATS_QUEUE_LIMIT, NATS_SUBJECT,
|
||||||
PULSAR_QUEUE_DIR, PULSAR_QUEUE_LIMIT, PULSAR_TLS_ALLOW_INSECURE, PULSAR_TLS_CA, PULSAR_TLS_HOSTNAME_VERIFICATION,
|
NATS_TLS_CA, NATS_TLS_CLIENT_CERT, NATS_TLS_CLIENT_KEY, NATS_TLS_REQUIRED, NATS_TOKEN, NATS_USERNAME, PULSAR_AUTH_TOKEN,
|
||||||
PULSAR_TOPIC, PULSAR_USERNAME, WEBHOOK_AUTH_TOKEN, WEBHOOK_BATCH_SIZE, WEBHOOK_CLIENT_CA, WEBHOOK_CLIENT_CERT,
|
PULSAR_BROKER, PULSAR_PASSWORD, PULSAR_QUEUE_DIR, PULSAR_QUEUE_LIMIT, PULSAR_TLS_ALLOW_INSECURE, PULSAR_TLS_CA,
|
||||||
WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_HTTP_TIMEOUT, WEBHOOK_MAX_RETRY, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT,
|
PULSAR_TLS_HOSTNAME_VERIFICATION, PULSAR_TOPIC, PULSAR_USERNAME, WEBHOOK_AUTH_TOKEN, WEBHOOK_BATCH_SIZE, WEBHOOK_CLIENT_CA,
|
||||||
WEBHOOK_RETRY_INTERVAL, WEBHOOK_SKIP_TLS_VERIFY,
|
WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_HTTP_TIMEOUT, WEBHOOK_MAX_RETRY, WEBHOOK_QUEUE_DIR,
|
||||||
|
WEBHOOK_QUEUE_LIMIT, WEBHOOK_RETRY_INTERVAL, WEBHOOK_SKIP_TLS_VERIFY,
|
||||||
};
|
};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
@@ -337,3 +338,63 @@ pub static DEFAULT_AUDIT_PULSAR_KVS: LazyLock<KVS> = LazyLock::new(|| {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pub static DEFAULT_AUDIT_KAFKA_KVS: LazyLock<KVS> = LazyLock::new(|| {
|
||||||
|
KVS(vec![
|
||||||
|
KV {
|
||||||
|
key: ENABLE_KEY.to_owned(),
|
||||||
|
value: EnableState::Off.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: KAFKA_BROKERS.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: KAFKA_TOPIC.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: KAFKA_ACKS.to_owned(),
|
||||||
|
value: "1".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: KAFKA_TLS_ENABLE.to_owned(),
|
||||||
|
value: EnableState::Off.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: KAFKA_TLS_CA.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: KAFKA_TLS_CLIENT_CERT.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: KAFKA_TLS_CLIENT_KEY.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: KAFKA_QUEUE_DIR.to_owned(),
|
||||||
|
value: EVENT_DEFAULT_DIR.to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: KAFKA_QUEUE_LIMIT.to_owned(),
|
||||||
|
value: DEFAULT_LIMIT.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: COMMENT_KEY.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ use crate::global::is_first_cluster_node_local;
|
|||||||
use crate::store_api::{ObjectInfo, ObjectOptions, PutObjReader, StorageAPI};
|
use crate::store_api::{ObjectInfo, ObjectOptions, PutObjReader, StorageAPI};
|
||||||
use http::HeaderMap;
|
use http::HeaderMap;
|
||||||
use rustfs_config::audit::{
|
use rustfs_config::audit::{
|
||||||
AUDIT_MQTT_KEYS, AUDIT_MQTT_SUB_SYS, AUDIT_NATS_KEYS, AUDIT_NATS_SUB_SYS, AUDIT_PULSAR_KEYS, AUDIT_PULSAR_SUB_SYS,
|
AUDIT_KAFKA_KEYS, AUDIT_KAFKA_SUB_SYS, AUDIT_MQTT_KEYS, AUDIT_MQTT_SUB_SYS, AUDIT_NATS_KEYS, AUDIT_NATS_SUB_SYS,
|
||||||
AUDIT_WEBHOOK_KEYS, AUDIT_WEBHOOK_SUB_SYS,
|
AUDIT_PULSAR_KEYS, AUDIT_PULSAR_SUB_SYS, AUDIT_WEBHOOK_KEYS, AUDIT_WEBHOOK_SUB_SYS,
|
||||||
};
|
};
|
||||||
use rustfs_config::notify::{
|
use rustfs_config::notify::{
|
||||||
NOTIFY_MQTT_KEYS, NOTIFY_MQTT_SUB_SYS, NOTIFY_NATS_KEYS, NOTIFY_NATS_SUB_SYS, NOTIFY_PULSAR_KEYS, NOTIFY_PULSAR_SUB_SYS,
|
NOTIFY_KAFKA_KEYS, NOTIFY_KAFKA_SUB_SYS, NOTIFY_MQTT_KEYS, NOTIFY_MQTT_SUB_SYS, NOTIFY_NATS_KEYS, NOTIFY_NATS_SUB_SYS,
|
||||||
NOTIFY_WEBHOOK_KEYS, NOTIFY_WEBHOOK_SUB_SYS,
|
NOTIFY_PULSAR_KEYS, NOTIFY_PULSAR_SUB_SYS, NOTIFY_WEBHOOK_KEYS, NOTIFY_WEBHOOK_SUB_SYS,
|
||||||
};
|
};
|
||||||
use rustfs_config::oidc::{IDENTITY_OPENID_KEYS, IDENTITY_OPENID_SUB_SYS, OIDC_REDIRECT_URI_DYNAMIC};
|
use rustfs_config::oidc::{IDENTITY_OPENID_KEYS, IDENTITY_OPENID_SUB_SYS, OIDC_REDIRECT_URI_DYNAMIC};
|
||||||
use rustfs_config::{COMMENT_KEY, DEFAULT_DELIMITER, ENABLE_KEY, EnableState, RUSTFS_REGION};
|
use rustfs_config::{COMMENT_KEY, DEFAULT_DELIMITER, ENABLE_KEY, EnableState, RUSTFS_REGION};
|
||||||
@@ -58,7 +58,7 @@ struct TargetConfigDescriptor {
|
|||||||
valid_keys: &'static [&'static str],
|
valid_keys: &'static [&'static str],
|
||||||
}
|
}
|
||||||
|
|
||||||
fn notify_target_descriptors() -> [TargetConfigDescriptor; 4] {
|
fn notify_target_descriptors() -> [TargetConfigDescriptor; 5] {
|
||||||
[
|
[
|
||||||
TargetConfigDescriptor {
|
TargetConfigDescriptor {
|
||||||
external_key: "webhook",
|
external_key: "webhook",
|
||||||
@@ -66,6 +66,12 @@ fn notify_target_descriptors() -> [TargetConfigDescriptor; 4] {
|
|||||||
default_kvs: ¬ify::DEFAULT_NOTIFY_WEBHOOK_KVS,
|
default_kvs: ¬ify::DEFAULT_NOTIFY_WEBHOOK_KVS,
|
||||||
valid_keys: NOTIFY_WEBHOOK_KEYS,
|
valid_keys: NOTIFY_WEBHOOK_KEYS,
|
||||||
},
|
},
|
||||||
|
TargetConfigDescriptor {
|
||||||
|
external_key: "kafka",
|
||||||
|
subsystem_key: NOTIFY_KAFKA_SUB_SYS,
|
||||||
|
default_kvs: ¬ify::DEFAULT_NOTIFY_KAFKA_KVS,
|
||||||
|
valid_keys: NOTIFY_KAFKA_KEYS,
|
||||||
|
},
|
||||||
TargetConfigDescriptor {
|
TargetConfigDescriptor {
|
||||||
external_key: "mqtt",
|
external_key: "mqtt",
|
||||||
subsystem_key: NOTIFY_MQTT_SUB_SYS,
|
subsystem_key: NOTIFY_MQTT_SUB_SYS,
|
||||||
@@ -87,7 +93,7 @@ fn notify_target_descriptors() -> [TargetConfigDescriptor; 4] {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn audit_target_descriptors() -> [TargetConfigDescriptor; 4] {
|
fn audit_target_descriptors() -> [TargetConfigDescriptor; 5] {
|
||||||
[
|
[
|
||||||
TargetConfigDescriptor {
|
TargetConfigDescriptor {
|
||||||
external_key: "webhook",
|
external_key: "webhook",
|
||||||
@@ -95,6 +101,12 @@ fn audit_target_descriptors() -> [TargetConfigDescriptor; 4] {
|
|||||||
default_kvs: &audit::DEFAULT_AUDIT_WEBHOOK_KVS,
|
default_kvs: &audit::DEFAULT_AUDIT_WEBHOOK_KVS,
|
||||||
valid_keys: AUDIT_WEBHOOK_KEYS,
|
valid_keys: AUDIT_WEBHOOK_KEYS,
|
||||||
},
|
},
|
||||||
|
TargetConfigDescriptor {
|
||||||
|
external_key: "kafka",
|
||||||
|
subsystem_key: AUDIT_KAFKA_SUB_SYS,
|
||||||
|
default_kvs: &audit::DEFAULT_AUDIT_KAFKA_KVS,
|
||||||
|
valid_keys: AUDIT_KAFKA_KEYS,
|
||||||
|
},
|
||||||
TargetConfigDescriptor {
|
TargetConfigDescriptor {
|
||||||
external_key: "mqtt",
|
external_key: "mqtt",
|
||||||
subsystem_key: AUDIT_MQTT_SUB_SYS,
|
subsystem_key: AUDIT_MQTT_SUB_SYS,
|
||||||
@@ -335,7 +347,7 @@ fn apply_external_oidc_map(cfg: &mut Config, root: &Map<String, Value>) -> bool
|
|||||||
applied
|
applied
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_notify_scalar_value(key: &str, value: &Value) -> Option<String> {
|
fn parse_target_scalar_value(key: &str, value: &Value) -> Option<String> {
|
||||||
match value {
|
match value {
|
||||||
Value::String(v) => Some(v.trim().to_string()),
|
Value::String(v) => Some(v.trim().to_string()),
|
||||||
Value::Bool(v) if key == ENABLE_KEY || key == rustfs_config::WEBHOOK_SKIP_TLS_VERIFY => Some(if *v {
|
Value::Bool(v) if key == ENABLE_KEY || key == rustfs_config::WEBHOOK_SKIP_TLS_VERIFY => Some(if *v {
|
||||||
@@ -350,7 +362,7 @@ fn parse_notify_scalar_value(key: &str, value: &Value) -> Option<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_notify_instance_object(instance: &Map<String, Value>, valid_keys: &[&str]) -> KVS {
|
fn decode_target_instance_object(instance: &Map<String, Value>, valid_keys: &[&str]) -> KVS {
|
||||||
let mut kvs = KVS::new();
|
let mut kvs = KVS::new();
|
||||||
|
|
||||||
for (key, value) in instance {
|
for (key, value) in instance {
|
||||||
@@ -358,7 +370,7 @@ fn decode_notify_instance_object(instance: &Map<String, Value>, valid_keys: &[&s
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(parsed) = parse_notify_scalar_value(key, value) {
|
if let Some(parsed) = parse_target_scalar_value(key, value) {
|
||||||
kvs.insert(key.clone(), parsed);
|
kvs.insert(key.clone(), parsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,21 +378,21 @@ fn decode_notify_instance_object(instance: &Map<String, Value>, valid_keys: &[&s
|
|||||||
kvs
|
kvs
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_notify_instance_value(value: &Value, valid_keys: &[&str]) -> Option<KVS> {
|
fn decode_target_instance_value(value: &Value, valid_keys: &[&str]) -> Option<KVS> {
|
||||||
match value {
|
match value {
|
||||||
Value::Object(instance) => Some(decode_notify_instance_object(instance, valid_keys)),
|
Value::Object(instance) => Some(decode_target_instance_object(instance, valid_keys)),
|
||||||
Value::Array(_) => serde_json::from_value::<KVS>(value.clone()).ok(),
|
Value::Array(_) => serde_json::from_value::<KVS>(value.clone()).ok(),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_notify_instance_shorthand(section: &Map<String, Value>, valid_keys: &[&str]) -> bool {
|
fn is_target_instance_shorthand(section: &Map<String, Value>, valid_keys: &[&str]) -> bool {
|
||||||
section
|
section
|
||||||
.iter()
|
.iter()
|
||||||
.any(|(key, value)| valid_keys.contains(&key.as_str()) && parse_notify_scalar_value(key, value).is_some())
|
.any(|(key, value)| valid_keys.contains(&key.as_str()) && parse_target_scalar_value(key, value).is_some())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_external_notify_section(
|
fn apply_external_target_section(
|
||||||
cfg: &mut Config,
|
cfg: &mut Config,
|
||||||
notify_obj: &Map<String, Value>,
|
notify_obj: &Map<String, Value>,
|
||||||
external_key: &str,
|
external_key: &str,
|
||||||
@@ -399,8 +411,8 @@ fn apply_external_notify_section(
|
|||||||
let subsystem = cfg.0.entry(subsystem_key.to_string()).or_default();
|
let subsystem = cfg.0.entry(subsystem_key.to_string()).or_default();
|
||||||
let mut applied = false;
|
let mut applied = false;
|
||||||
|
|
||||||
if is_notify_instance_shorthand(section_obj, valid_keys) {
|
if is_target_instance_shorthand(section_obj, valid_keys) {
|
||||||
let kvs = decode_notify_instance_object(section_obj, valid_keys);
|
let kvs = decode_target_instance_object(section_obj, valid_keys);
|
||||||
if !kvs.is_empty() {
|
if !kvs.is_empty() {
|
||||||
let mut merged = default_kvs.clone();
|
let mut merged = default_kvs.clone();
|
||||||
merged.extend(kvs);
|
merged.extend(kvs);
|
||||||
@@ -411,7 +423,7 @@ fn apply_external_notify_section(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (raw_instance, value) in section_obj {
|
for (raw_instance, value) in section_obj {
|
||||||
let Some(mut kvs) = decode_notify_instance_value(value, valid_keys) else {
|
let Some(mut kvs) = decode_target_instance_value(value, valid_keys) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if kvs.is_empty() {
|
if kvs.is_empty() {
|
||||||
@@ -444,7 +456,7 @@ fn apply_external_target_descriptors(
|
|||||||
) -> bool {
|
) -> bool {
|
||||||
let mut applied = false;
|
let mut applied = false;
|
||||||
for descriptor in descriptors {
|
for descriptor in descriptors {
|
||||||
applied |= apply_external_notify_section(
|
applied |= apply_external_target_section(
|
||||||
cfg,
|
cfg,
|
||||||
section_obj,
|
section_obj,
|
||||||
descriptor.external_key,
|
descriptor.external_key,
|
||||||
@@ -663,11 +675,12 @@ fn build_semantic_oidc_object(cfg: &Config) -> Map<String, Value> {
|
|||||||
oidc_obj
|
oidc_obj
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_notify_bool_key(key: &str) -> bool {
|
fn is_target_bool_key(key: &str) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
key,
|
key,
|
||||||
ENABLE_KEY
|
ENABLE_KEY
|
||||||
| rustfs_config::WEBHOOK_SKIP_TLS_VERIFY
|
| rustfs_config::WEBHOOK_SKIP_TLS_VERIFY
|
||||||
|
| rustfs_config::KAFKA_TLS_ENABLE
|
||||||
| rustfs_config::MQTT_TLS_TRUST_LEAF_AS_CA
|
| rustfs_config::MQTT_TLS_TRUST_LEAF_AS_CA
|
||||||
| rustfs_config::NATS_TLS_REQUIRED
|
| rustfs_config::NATS_TLS_REQUIRED
|
||||||
| rustfs_config::PULSAR_TLS_ALLOW_INSECURE
|
| rustfs_config::PULSAR_TLS_ALLOW_INSECURE
|
||||||
@@ -675,8 +688,8 @@ fn is_notify_bool_key(key: &str) -> bool {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn encode_notify_scalar_value(key: &str, value: &str) -> Value {
|
fn encode_target_scalar_value(key: &str, value: &str) -> Value {
|
||||||
if is_notify_bool_key(key) {
|
if is_target_bool_key(key) {
|
||||||
if let Ok(state) = value.parse::<EnableState>() {
|
if let Ok(state) = value.parse::<EnableState>() {
|
||||||
return Value::Bool(state.is_enabled());
|
return Value::Bool(state.is_enabled());
|
||||||
}
|
}
|
||||||
@@ -697,7 +710,7 @@ fn is_hidden_if_empty(default_kvs: &KVS, key: &str) -> bool {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_notify_instance_diff_object(kvs: &KVS, baseline: &KVS, valid_keys: &[&str], default_kvs: &KVS) -> Map<String, Value> {
|
fn build_target_instance_diff_object(kvs: &KVS, baseline: &KVS, valid_keys: &[&str], default_kvs: &KVS) -> Map<String, Value> {
|
||||||
let mut instance = Map::new();
|
let mut instance = Map::new();
|
||||||
|
|
||||||
for key in valid_keys {
|
for key in valid_keys {
|
||||||
@@ -720,13 +733,13 @@ fn build_notify_instance_diff_object(kvs: &KVS, baseline: &KVS, valid_keys: &[&s
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
instance.insert((*key).to_string(), encode_notify_scalar_value(key, &effective_value));
|
instance.insert((*key).to_string(), encode_target_scalar_value(key, &effective_value));
|
||||||
}
|
}
|
||||||
|
|
||||||
instance
|
instance
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merged_notify_default_kvs(subsystem: &HashMap<String, KVS>, default_kvs: &KVS) -> KVS {
|
fn merged_target_default_kvs(subsystem: &HashMap<String, KVS>, default_kvs: &KVS) -> KVS {
|
||||||
let mut merged = default_kvs.clone();
|
let mut merged = default_kvs.clone();
|
||||||
if let Some(kvs) = subsystem.get(DEFAULT_DELIMITER) {
|
if let Some(kvs) = subsystem.get(DEFAULT_DELIMITER) {
|
||||||
merged.extend(kvs.clone());
|
merged.extend(kvs.clone());
|
||||||
@@ -734,7 +747,7 @@ fn merged_notify_default_kvs(subsystem: &HashMap<String, KVS>, default_kvs: &KVS
|
|||||||
merged
|
merged
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_notify_subsystem_object(
|
fn build_target_subsystem_object(
|
||||||
cfg: &Config,
|
cfg: &Config,
|
||||||
subsystem_key: &str,
|
subsystem_key: &str,
|
||||||
default_kvs: &KVS,
|
default_kvs: &KVS,
|
||||||
@@ -744,11 +757,11 @@ fn build_notify_subsystem_object(
|
|||||||
return Map::new();
|
return Map::new();
|
||||||
};
|
};
|
||||||
|
|
||||||
let effective_default = merged_notify_default_kvs(subsystem, default_kvs);
|
let effective_default = merged_target_default_kvs(subsystem, default_kvs);
|
||||||
let mut subsystem_obj = Map::new();
|
let mut subsystem_obj = Map::new();
|
||||||
|
|
||||||
if let Some(default_instance) = subsystem.get(DEFAULT_DELIMITER) {
|
if let Some(default_instance) = subsystem.get(DEFAULT_DELIMITER) {
|
||||||
let default_obj = build_notify_instance_diff_object(default_instance, default_kvs, valid_keys, default_kvs);
|
let default_obj = build_target_instance_diff_object(default_instance, default_kvs, valid_keys, default_kvs);
|
||||||
if !default_obj.is_empty() {
|
if !default_obj.is_empty() {
|
||||||
subsystem_obj.insert("default".to_string(), Value::Object(default_obj));
|
subsystem_obj.insert("default".to_string(), Value::Object(default_obj));
|
||||||
}
|
}
|
||||||
@@ -761,7 +774,7 @@ fn build_notify_subsystem_object(
|
|||||||
instances.sort_by_key(|(lhs, _)| *lhs);
|
instances.sort_by_key(|(lhs, _)| *lhs);
|
||||||
|
|
||||||
for (instance_key, kvs) in instances {
|
for (instance_key, kvs) in instances {
|
||||||
let instance_obj = build_notify_instance_diff_object(kvs, &effective_default, valid_keys, default_kvs);
|
let instance_obj = build_target_instance_diff_object(kvs, &effective_default, valid_keys, default_kvs);
|
||||||
if !instance_obj.is_empty() {
|
if !instance_obj.is_empty() {
|
||||||
subsystem_obj.insert(instance_key.clone(), Value::Object(instance_obj));
|
subsystem_obj.insert(instance_key.clone(), Value::Object(instance_obj));
|
||||||
}
|
}
|
||||||
@@ -774,7 +787,7 @@ fn build_target_object(cfg: &Config, descriptors: &[TargetConfigDescriptor]) ->
|
|||||||
let mut target_obj = Map::new();
|
let mut target_obj = Map::new();
|
||||||
for descriptor in descriptors {
|
for descriptor in descriptors {
|
||||||
let subsystem_obj =
|
let subsystem_obj =
|
||||||
build_notify_subsystem_object(cfg, descriptor.subsystem_key, descriptor.default_kvs, descriptor.valid_keys);
|
build_target_subsystem_object(cfg, descriptor.subsystem_key, descriptor.default_kvs, descriptor.valid_keys);
|
||||||
if !subsystem_obj.is_empty() {
|
if !subsystem_obj.is_empty() {
|
||||||
target_obj.insert(descriptor.external_key.to_string(), Value::Object(subsystem_obj));
|
target_obj.insert(descriptor.external_key.to_string(), Value::Object(subsystem_obj));
|
||||||
}
|
}
|
||||||
@@ -1118,8 +1131,8 @@ mod tests {
|
|||||||
ObjectOptions, ObjectToDelete, PartInfo, PutObjReader, StorageAPI, WalkOptions,
|
ObjectOptions, ObjectToDelete, PartInfo, PutObjReader, StorageAPI, WalkOptions,
|
||||||
};
|
};
|
||||||
use http::HeaderMap;
|
use http::HeaderMap;
|
||||||
use rustfs_config::audit::{AUDIT_MQTT_SUB_SYS, AUDIT_WEBHOOK_SUB_SYS};
|
use rustfs_config::audit::{AUDIT_KAFKA_SUB_SYS, AUDIT_MQTT_SUB_SYS, AUDIT_WEBHOOK_SUB_SYS};
|
||||||
use rustfs_config::notify::{NOTIFY_MQTT_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS};
|
use rustfs_config::notify::{NOTIFY_KAFKA_SUB_SYS, NOTIFY_MQTT_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS};
|
||||||
use rustfs_config::oidc::IDENTITY_OPENID_SUB_SYS;
|
use rustfs_config::oidc::IDENTITY_OPENID_SUB_SYS;
|
||||||
use rustfs_config::{DEFAULT_DELIMITER, ENABLE_KEY, EnableState};
|
use rustfs_config::{DEFAULT_DELIMITER, ENABLE_KEY, EnableState};
|
||||||
use rustfs_filemeta::FileInfo;
|
use rustfs_filemeta::FileInfo;
|
||||||
@@ -1753,6 +1766,15 @@ mod tests {
|
|||||||
"topic":"events",
|
"topic":"events",
|
||||||
"queue_dir":""
|
"queue_dir":""
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"kafka":{
|
||||||
|
"streaming":{
|
||||||
|
"enable":true,
|
||||||
|
"brokers":"127.0.0.1:9092,127.0.0.1:9093",
|
||||||
|
"topic":"events-kafka",
|
||||||
|
"acks":"all",
|
||||||
|
"tls_enable":true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}"#;
|
}"#;
|
||||||
@@ -1781,6 +1803,14 @@ mod tests {
|
|||||||
.expect("mqtt target should be decoded");
|
.expect("mqtt target should be decoded");
|
||||||
assert_eq!(mqtt.get(rustfs_config::MQTT_BROKER), "tcp://127.0.0.1:1883");
|
assert_eq!(mqtt.get(rustfs_config::MQTT_BROKER), "tcp://127.0.0.1:1883");
|
||||||
assert_eq!(mqtt.get(rustfs_config::MQTT_QUEUE_DIR), "");
|
assert_eq!(mqtt.get(rustfs_config::MQTT_QUEUE_DIR), "");
|
||||||
|
|
||||||
|
let kafka = cfg
|
||||||
|
.get_value(NOTIFY_KAFKA_SUB_SYS, "streaming")
|
||||||
|
.expect("kafka target should be decoded");
|
||||||
|
assert_eq!(kafka.get(rustfs_config::KAFKA_BROKERS), "127.0.0.1:9092,127.0.0.1:9093");
|
||||||
|
assert_eq!(kafka.get(rustfs_config::KAFKA_TOPIC), "events-kafka");
|
||||||
|
assert_eq!(kafka.get(rustfs_config::KAFKA_ACKS), "all");
|
||||||
|
assert_eq!(kafka.get(rustfs_config::KAFKA_TLS_ENABLE), "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1850,6 +1880,14 @@ mod tests {
|
|||||||
"broker":"tcp://127.0.0.1:1883",
|
"broker":"tcp://127.0.0.1:1883",
|
||||||
"topic":"audit-events"
|
"topic":"audit-events"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"kafka":{
|
||||||
|
"auditlog":{
|
||||||
|
"enable":true,
|
||||||
|
"brokers":"127.0.0.1:9092",
|
||||||
|
"topic":"audit-events-kafka",
|
||||||
|
"acks":"1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}"#;
|
}"#;
|
||||||
@@ -1873,6 +1911,12 @@ mod tests {
|
|||||||
.get_value(AUDIT_MQTT_SUB_SYS, "analytics")
|
.get_value(AUDIT_MQTT_SUB_SYS, "analytics")
|
||||||
.expect("audit mqtt target should be decoded");
|
.expect("audit mqtt target should be decoded");
|
||||||
assert_eq!(mqtt.get(rustfs_config::MQTT_BROKER), "tcp://127.0.0.1:1883");
|
assert_eq!(mqtt.get(rustfs_config::MQTT_BROKER), "tcp://127.0.0.1:1883");
|
||||||
|
|
||||||
|
let kafka = cfg
|
||||||
|
.get_value(AUDIT_KAFKA_SUB_SYS, "auditlog")
|
||||||
|
.expect("audit kafka target should be decoded");
|
||||||
|
assert_eq!(kafka.get(rustfs_config::KAFKA_BROKERS), "127.0.0.1:9092");
|
||||||
|
assert_eq!(kafka.get(rustfs_config::KAFKA_TOPIC), "audit-events-kafka");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1993,6 +2037,38 @@ mod tests {
|
|||||||
);
|
);
|
||||||
cfg.0.insert(NOTIFY_MQTT_SUB_SYS.to_string(), mqtt_section);
|
cfg.0.insert(NOTIFY_MQTT_SUB_SYS.to_string(), mqtt_section);
|
||||||
|
|
||||||
|
let mut kafka_default = notify::DEFAULT_NOTIFY_KAFKA_KVS.clone();
|
||||||
|
kafka_default.insert(ENABLE_KEY.to_string(), EnableState::On.to_string());
|
||||||
|
kafka_default.insert(rustfs_config::KAFKA_TOPIC.to_string(), "events-kafka".to_string());
|
||||||
|
let mut kafka_section = std::collections::HashMap::new();
|
||||||
|
kafka_section.insert(DEFAULT_DELIMITER.to_string(), kafka_default);
|
||||||
|
kafka_section.insert(
|
||||||
|
"streaming".to_string(),
|
||||||
|
crate::config::KVS(vec![
|
||||||
|
crate::config::KV {
|
||||||
|
key: ENABLE_KEY.to_string(),
|
||||||
|
value: EnableState::On.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
crate::config::KV {
|
||||||
|
key: rustfs_config::KAFKA_BROKERS.to_string(),
|
||||||
|
value: "127.0.0.1:9092,127.0.0.1:9093".to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
crate::config::KV {
|
||||||
|
key: rustfs_config::KAFKA_ACKS.to_string(),
|
||||||
|
value: "all".to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
crate::config::KV {
|
||||||
|
key: rustfs_config::KAFKA_TLS_ENABLE.to_string(),
|
||||||
|
value: EnableState::On.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
cfg.0.insert(NOTIFY_KAFKA_SUB_SYS.to_string(), kafka_section);
|
||||||
|
|
||||||
let out = encode_server_config_blob(&cfg, None).expect("encode should succeed");
|
let out = encode_server_config_blob(&cfg, None).expect("encode should succeed");
|
||||||
let v: Value = serde_json::from_slice(&out).expect("output should be json");
|
let v: Value = serde_json::from_slice(&out).expect("output should be json");
|
||||||
let notify = v
|
let notify = v
|
||||||
@@ -2028,6 +2104,19 @@ mod tests {
|
|||||||
.expect("mqtt target should be encoded");
|
.expect("mqtt target should be encoded");
|
||||||
assert_eq!(mqtt.get(rustfs_config::MQTT_BROKER).and_then(Value::as_str), Some("tcp://127.0.0.1:1883"));
|
assert_eq!(mqtt.get(rustfs_config::MQTT_BROKER).and_then(Value::as_str), Some("tcp://127.0.0.1:1883"));
|
||||||
assert_eq!(mqtt.get(rustfs_config::MQTT_QUEUE_DIR).and_then(Value::as_str), Some(""));
|
assert_eq!(mqtt.get(rustfs_config::MQTT_QUEUE_DIR).and_then(Value::as_str), Some(""));
|
||||||
|
|
||||||
|
let kafka = notify
|
||||||
|
.get("kafka")
|
||||||
|
.and_then(Value::as_object)
|
||||||
|
.and_then(|targets| targets.get("streaming"))
|
||||||
|
.and_then(Value::as_object)
|
||||||
|
.expect("kafka target should be encoded");
|
||||||
|
assert_eq!(
|
||||||
|
kafka.get(rustfs_config::KAFKA_BROKERS).and_then(Value::as_str),
|
||||||
|
Some("127.0.0.1:9092,127.0.0.1:9093")
|
||||||
|
);
|
||||||
|
assert_eq!(kafka.get(rustfs_config::KAFKA_ACKS).and_then(Value::as_str), Some("all"));
|
||||||
|
assert_eq!(kafka.get(rustfs_config::KAFKA_TLS_ENABLE).and_then(Value::as_bool), Some(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2079,6 +2168,28 @@ mod tests {
|
|||||||
);
|
);
|
||||||
cfg.0.insert(AUDIT_MQTT_SUB_SYS.to_string(), mqtt_section);
|
cfg.0.insert(AUDIT_MQTT_SUB_SYS.to_string(), mqtt_section);
|
||||||
|
|
||||||
|
let mut kafka_default = audit::DEFAULT_AUDIT_KAFKA_KVS.clone();
|
||||||
|
kafka_default.insert(ENABLE_KEY.to_string(), EnableState::On.to_string());
|
||||||
|
kafka_default.insert(rustfs_config::KAFKA_TOPIC.to_string(), "audit-events-kafka".to_string());
|
||||||
|
let mut kafka_section = std::collections::HashMap::new();
|
||||||
|
kafka_section.insert(DEFAULT_DELIMITER.to_string(), kafka_default);
|
||||||
|
kafka_section.insert(
|
||||||
|
"auditlog".to_string(),
|
||||||
|
crate::config::KVS(vec![
|
||||||
|
crate::config::KV {
|
||||||
|
key: ENABLE_KEY.to_string(),
|
||||||
|
value: EnableState::On.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
crate::config::KV {
|
||||||
|
key: rustfs_config::KAFKA_BROKERS.to_string(),
|
||||||
|
value: "127.0.0.1:9092".to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
cfg.0.insert(AUDIT_KAFKA_SUB_SYS.to_string(), kafka_section);
|
||||||
|
|
||||||
let out = encode_server_config_blob(&cfg, None).expect("encode should succeed");
|
let out = encode_server_config_blob(&cfg, None).expect("encode should succeed");
|
||||||
let v: Value = serde_json::from_slice(&out).expect("output should be json");
|
let v: Value = serde_json::from_slice(&out).expect("output should be json");
|
||||||
let logger = v
|
let logger = v
|
||||||
@@ -2105,6 +2216,14 @@ mod tests {
|
|||||||
.expect("audit mqtt default should be encoded");
|
.expect("audit mqtt default should be encoded");
|
||||||
assert_eq!(mqtt_default.get(ENABLE_KEY).and_then(Value::as_bool), Some(true));
|
assert_eq!(mqtt_default.get(ENABLE_KEY).and_then(Value::as_bool), Some(true));
|
||||||
assert_eq!(mqtt_default.get(rustfs_config::MQTT_TOPIC).and_then(Value::as_str), Some("audit-events"));
|
assert_eq!(mqtt_default.get(rustfs_config::MQTT_TOPIC).and_then(Value::as_str), Some("audit-events"));
|
||||||
|
|
||||||
|
let kafka = logger
|
||||||
|
.get("kafka")
|
||||||
|
.and_then(Value::as_object)
|
||||||
|
.and_then(|targets| targets.get("auditlog"))
|
||||||
|
.and_then(Value::as_object)
|
||||||
|
.expect("audit kafka target should be encoded");
|
||||||
|
assert_eq!(kafka.get(rustfs_config::KAFKA_BROKERS).and_then(Value::as_str), Some("127.0.0.1:9092"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -25,8 +25,12 @@ use crate::store::ECStore;
|
|||||||
use com::{STORAGE_CLASS_SUB_SYS, lookup_configs, read_config_without_migrate};
|
use com::{STORAGE_CLASS_SUB_SYS, lookup_configs, read_config_without_migrate};
|
||||||
use rustfs_config::COMMENT_KEY;
|
use rustfs_config::COMMENT_KEY;
|
||||||
use rustfs_config::DEFAULT_DELIMITER;
|
use rustfs_config::DEFAULT_DELIMITER;
|
||||||
use rustfs_config::audit::{AUDIT_MQTT_SUB_SYS, AUDIT_NATS_SUB_SYS, AUDIT_PULSAR_SUB_SYS, AUDIT_WEBHOOK_SUB_SYS};
|
use rustfs_config::audit::{
|
||||||
use rustfs_config::notify::{NOTIFY_MQTT_SUB_SYS, NOTIFY_NATS_SUB_SYS, NOTIFY_PULSAR_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS};
|
AUDIT_KAFKA_SUB_SYS, AUDIT_MQTT_SUB_SYS, AUDIT_NATS_SUB_SYS, AUDIT_PULSAR_SUB_SYS, AUDIT_WEBHOOK_SUB_SYS,
|
||||||
|
};
|
||||||
|
use rustfs_config::notify::{
|
||||||
|
NOTIFY_KAFKA_SUB_SYS, NOTIFY_MQTT_SUB_SYS, NOTIFY_NATS_SUB_SYS, NOTIFY_PULSAR_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS,
|
||||||
|
};
|
||||||
use rustfs_config::oidc::IDENTITY_OPENID_SUB_SYS;
|
use rustfs_config::oidc::IDENTITY_OPENID_SUB_SYS;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -38,10 +42,6 @@ pub static DEFAULT_KVS: LazyLock<OnceLock<HashMap<String, KVS>>> = LazyLock::new
|
|||||||
pub static GLOBAL_SERVER_CONFIG: LazyLock<OnceLock<Config>> = LazyLock::new(OnceLock::new);
|
pub static GLOBAL_SERVER_CONFIG: LazyLock<OnceLock<Config>> = LazyLock::new(OnceLock::new);
|
||||||
pub static GLOBAL_CONFIG_SYS: LazyLock<ConfigSys> = LazyLock::new(ConfigSys::new);
|
pub static GLOBAL_CONFIG_SYS: LazyLock<ConfigSys> = LazyLock::new(ConfigSys::new);
|
||||||
|
|
||||||
pub const ENV_ACCESS_KEY: &str = "RUSTFS_ACCESS_KEY";
|
|
||||||
pub const ENV_SECRET_KEY: &str = "RUSTFS_SECRET_KEY";
|
|
||||||
pub const ENV_ROOT_USER: &str = "RUSTFS_ROOT_USER";
|
|
||||||
pub const ENV_ROOT_PASSWORD: &str = "RUSTFS_ROOT_PASSWORD";
|
|
||||||
pub static RUSTFS_CONFIG_PREFIX: &str = "config";
|
pub static RUSTFS_CONFIG_PREFIX: &str = "config";
|
||||||
|
|
||||||
pub struct ConfigSys {}
|
pub struct ConfigSys {}
|
||||||
@@ -245,6 +245,8 @@ pub fn init() {
|
|||||||
kvs.insert(AUDIT_NATS_SUB_SYS.to_owned(), audit::DEFAULT_AUDIT_NATS_KVS.clone());
|
kvs.insert(AUDIT_NATS_SUB_SYS.to_owned(), audit::DEFAULT_AUDIT_NATS_KVS.clone());
|
||||||
kvs.insert(NOTIFY_PULSAR_SUB_SYS.to_owned(), notify::DEFAULT_NOTIFY_PULSAR_KVS.clone());
|
kvs.insert(NOTIFY_PULSAR_SUB_SYS.to_owned(), notify::DEFAULT_NOTIFY_PULSAR_KVS.clone());
|
||||||
kvs.insert(AUDIT_PULSAR_SUB_SYS.to_owned(), audit::DEFAULT_AUDIT_PULSAR_KVS.clone());
|
kvs.insert(AUDIT_PULSAR_SUB_SYS.to_owned(), audit::DEFAULT_AUDIT_PULSAR_KVS.clone());
|
||||||
|
kvs.insert(NOTIFY_KAFKA_SUB_SYS.to_owned(), notify::DEFAULT_NOTIFY_KAFKA_KVS.clone());
|
||||||
|
kvs.insert(AUDIT_KAFKA_SUB_SYS.to_owned(), audit::DEFAULT_AUDIT_KAFKA_KVS.clone());
|
||||||
kvs.insert(IDENTITY_OPENID_SUB_SYS.to_owned(), oidc::DEFAULT_IDENTITY_OPENID_KVS.clone());
|
kvs.insert(IDENTITY_OPENID_SUB_SYS.to_owned(), oidc::DEFAULT_IDENTITY_OPENID_KVS.clone());
|
||||||
|
|
||||||
// Register all default configurations
|
// Register all default configurations
|
||||||
|
|||||||
@@ -14,14 +14,15 @@
|
|||||||
|
|
||||||
use crate::config::{KV, KVS};
|
use crate::config::{KV, KVS};
|
||||||
use rustfs_config::{
|
use rustfs_config::{
|
||||||
COMMENT_KEY, DEFAULT_LIMIT, ENABLE_KEY, EVENT_DEFAULT_DIR, EnableState, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD,
|
COMMENT_KEY, DEFAULT_LIMIT, ENABLE_KEY, EVENT_DEFAULT_DIR, EnableState, KAFKA_ACKS, KAFKA_BROKERS, KAFKA_QUEUE_DIR,
|
||||||
MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TLS_CA, MQTT_TLS_CLIENT_CERT, MQTT_TLS_CLIENT_KEY,
|
KAFKA_QUEUE_LIMIT, KAFKA_TLS_CA, KAFKA_TLS_CLIENT_CERT, KAFKA_TLS_CLIENT_KEY, KAFKA_TLS_ENABLE, KAFKA_TOPIC, MQTT_BROKER,
|
||||||
MQTT_TLS_POLICY, MQTT_TLS_TRUST_LEAF_AS_CA, MQTT_TOPIC, MQTT_USERNAME, MQTT_WS_PATH_ALLOWLIST, NATS_ADDRESS,
|
MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD, MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TLS_CA,
|
||||||
NATS_CREDENTIALS_FILE, NATS_PASSWORD, NATS_QUEUE_DIR, NATS_QUEUE_LIMIT, NATS_SUBJECT, NATS_TLS_CA, NATS_TLS_CLIENT_CERT,
|
MQTT_TLS_CLIENT_CERT, MQTT_TLS_CLIENT_KEY, MQTT_TLS_POLICY, MQTT_TLS_TRUST_LEAF_AS_CA, MQTT_TOPIC, MQTT_USERNAME,
|
||||||
NATS_TLS_CLIENT_KEY, NATS_TLS_REQUIRED, NATS_TOKEN, NATS_USERNAME, PULSAR_AUTH_TOKEN, PULSAR_BROKER, PULSAR_PASSWORD,
|
MQTT_WS_PATH_ALLOWLIST, NATS_ADDRESS, NATS_CREDENTIALS_FILE, NATS_PASSWORD, NATS_QUEUE_DIR, NATS_QUEUE_LIMIT, NATS_SUBJECT,
|
||||||
PULSAR_QUEUE_DIR, PULSAR_QUEUE_LIMIT, PULSAR_TLS_ALLOW_INSECURE, PULSAR_TLS_CA, PULSAR_TLS_HOSTNAME_VERIFICATION,
|
NATS_TLS_CA, NATS_TLS_CLIENT_CERT, NATS_TLS_CLIENT_KEY, NATS_TLS_REQUIRED, NATS_TOKEN, NATS_USERNAME, PULSAR_AUTH_TOKEN,
|
||||||
PULSAR_TOPIC, PULSAR_USERNAME, WEBHOOK_AUTH_TOKEN, WEBHOOK_CLIENT_CA, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY,
|
PULSAR_BROKER, PULSAR_PASSWORD, PULSAR_QUEUE_DIR, PULSAR_QUEUE_LIMIT, PULSAR_TLS_ALLOW_INSECURE, PULSAR_TLS_CA,
|
||||||
WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT, WEBHOOK_SKIP_TLS_VERIFY,
|
PULSAR_TLS_HOSTNAME_VERIFICATION, PULSAR_TOPIC, PULSAR_USERNAME, WEBHOOK_AUTH_TOKEN, WEBHOOK_CLIENT_CA, WEBHOOK_CLIENT_CERT,
|
||||||
|
WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT, WEBHOOK_SKIP_TLS_VERIFY,
|
||||||
};
|
};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
@@ -314,3 +315,63 @@ pub static DEFAULT_NOTIFY_PULSAR_KVS: LazyLock<KVS> = LazyLock::new(|| {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pub static DEFAULT_NOTIFY_KAFKA_KVS: LazyLock<KVS> = LazyLock::new(|| {
|
||||||
|
KVS(vec![
|
||||||
|
KV {
|
||||||
|
key: ENABLE_KEY.to_owned(),
|
||||||
|
value: EnableState::Off.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: KAFKA_BROKERS.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: KAFKA_TOPIC.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: KAFKA_ACKS.to_owned(),
|
||||||
|
value: "1".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: KAFKA_TLS_ENABLE.to_owned(),
|
||||||
|
value: EnableState::Off.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: KAFKA_TLS_CA.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: KAFKA_TLS_CLIENT_CERT.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: KAFKA_TLS_CLIENT_KEY.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: true,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: KAFKA_QUEUE_DIR.to_owned(),
|
||||||
|
value: EVENT_DEFAULT_DIR.to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: KAFKA_QUEUE_LIMIT.to_owned(),
|
||||||
|
value: DEFAULT_LIMIT.to_string(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
KV {
|
||||||
|
key: COMMENT_KEY.to_owned(),
|
||||||
|
value: "".to_owned(),
|
||||||
|
hidden_if_empty: false,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|||||||
@@ -46,9 +46,7 @@ const DISK_HEALTH_FAULTY: u32 = 1;
|
|||||||
|
|
||||||
pub const ENV_RUSTFS_DRIVE_ACTIVE_MONITORING: &str = "RUSTFS_DRIVE_ACTIVE_MONITORING";
|
pub const ENV_RUSTFS_DRIVE_ACTIVE_MONITORING: &str = "RUSTFS_DRIVE_ACTIVE_MONITORING";
|
||||||
pub const DEFAULT_RUSTFS_DRIVE_ACTIVE_MONITORING: bool = true;
|
pub const DEFAULT_RUSTFS_DRIVE_ACTIVE_MONITORING: bool = true;
|
||||||
pub const CHECK_EVERY: Duration = Duration::from_secs(15);
|
|
||||||
pub const SKIP_IF_SUCCESS_BEFORE: Duration = Duration::from_secs(5);
|
pub const SKIP_IF_SUCCESS_BEFORE: Duration = Duration::from_secs(5);
|
||||||
pub const CHECK_TIMEOUT_DURATION: Duration = Duration::from_secs(5);
|
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
static ref TEST_DATA: Bytes = Bytes::from(vec![42u8; 2048]);
|
static ref TEST_DATA: Bytes = Bytes::from(vec![42u8; 2048]);
|
||||||
@@ -104,6 +102,20 @@ pub fn get_drive_walkdir_stall_timeout() -> Duration {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_drive_active_check_interval() -> Duration {
|
||||||
|
Duration::from_secs(rustfs_utils::get_env_u64(
|
||||||
|
rustfs_config::ENV_DRIVE_ACTIVE_CHECK_INTERVAL_SECS,
|
||||||
|
rustfs_config::DEFAULT_DRIVE_ACTIVE_CHECK_INTERVAL_SECS,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_drive_active_check_timeout() -> Duration {
|
||||||
|
Duration::from_secs(rustfs_utils::get_env_u64(
|
||||||
|
rustfs_config::ENV_DRIVE_ACTIVE_CHECK_TIMEOUT_SECS,
|
||||||
|
rustfs_config::DEFAULT_DRIVE_ACTIVE_CHECK_TIMEOUT_SECS,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
/// DiskHealthTracker tracks the health status of a disk.
|
/// DiskHealthTracker tracks the health status of a disk.
|
||||||
/// Similar to Go's diskHealthTracker.
|
/// Similar to Go's diskHealthTracker.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -231,9 +243,26 @@ impl DiskHealthTracker {
|
|||||||
RuntimeDriveHealthState::Offline => RuntimeDriveHealthState::Offline,
|
RuntimeDriveHealthState::Offline => RuntimeDriveHealthState::Offline,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.status.store(DISK_HEALTH_FAULTY, Ordering::Release);
|
let became_offline = next == RuntimeDriveHealthState::Offline && current != RuntimeDriveHealthState::Offline;
|
||||||
|
if next == RuntimeDriveHealthState::Offline {
|
||||||
|
self.status.store(DISK_HEALTH_FAULTY, Ordering::Release);
|
||||||
|
} else {
|
||||||
|
self.status.store(DISK_HEALTH_OK, Ordering::Release);
|
||||||
|
}
|
||||||
self.transition_state(endpoint, current, next, reason);
|
self.transition_state(endpoint, current, next, reason);
|
||||||
current == RuntimeDriveHealthState::Online
|
became_offline
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mark_offline(&self, endpoint: &Endpoint, reason: &'static str) -> bool {
|
||||||
|
let current = self.runtime_state();
|
||||||
|
if current == RuntimeDriveHealthState::Offline {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.consecutive_successes.store(0, Ordering::Release);
|
||||||
|
self.status.store(DISK_HEALTH_FAULTY, Ordering::Release);
|
||||||
|
self.transition_state(endpoint, current, RuntimeDriveHealthState::Offline, reason);
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mark_recovery_success(&self, endpoint: &Endpoint, reason: &'static str) -> bool {
|
pub fn mark_recovery_success(&self, endpoint: &Endpoint, reason: &'static str) -> bool {
|
||||||
@@ -268,6 +297,14 @@ impl DiskHealthTracker {
|
|||||||
became_online
|
became_online
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn record_operation_success(&self, endpoint: &Endpoint, reason: &'static str) {
|
||||||
|
if self.runtime_state() == RuntimeDriveHealthState::Online {
|
||||||
|
self.log_success();
|
||||||
|
} else {
|
||||||
|
self.mark_recovery_success(endpoint, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn transition_state(
|
fn transition_state(
|
||||||
&self,
|
&self,
|
||||||
endpoint: &Endpoint,
|
endpoint: &Endpoint,
|
||||||
@@ -449,7 +486,7 @@ impl LocalDiskWrapper {
|
|||||||
async fn monitor_disk_writable(disk: Arc<LocalDisk>, health: Arc<DiskHealthTracker>, cancel_token: CancellationToken) {
|
async fn monitor_disk_writable(disk: Arc<LocalDisk>, health: Arc<DiskHealthTracker>, cancel_token: CancellationToken) {
|
||||||
// TODO: config interval
|
// TODO: config interval
|
||||||
|
|
||||||
let mut interval = time::interval(CHECK_EVERY);
|
let mut interval = time::interval(get_drive_active_check_interval());
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
@@ -482,7 +519,16 @@ impl LocalDiskWrapper {
|
|||||||
|
|
||||||
|
|
||||||
let test_obj = format!("health-check-{}", Uuid::new_v4());
|
let test_obj = format!("health-check-{}", Uuid::new_v4());
|
||||||
if Self::perform_health_check(disk.clone(), &TEST_BUCKET, &test_obj, &TEST_DATA, true, CHECK_TIMEOUT_DURATION).await.is_err()
|
if Self::perform_health_check(
|
||||||
|
disk.clone(),
|
||||||
|
&TEST_BUCKET,
|
||||||
|
&test_obj,
|
||||||
|
&TEST_DATA,
|
||||||
|
true,
|
||||||
|
get_drive_active_check_timeout(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
&& health.mark_failure(&disk.endpoint(), "active_health_check_failed")
|
&& health.mark_failure(&disk.endpoint(), "active_health_check_failed")
|
||||||
{
|
{
|
||||||
// Health check failed, disk is considered faulty
|
// Health check failed, disk is considered faulty
|
||||||
@@ -588,7 +634,16 @@ impl LocalDiskWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let test_obj = format!("health-check-{}", Uuid::new_v4());
|
let test_obj = format!("health-check-{}", Uuid::new_v4());
|
||||||
match Self::perform_health_check(disk.clone(), &TEST_BUCKET, &test_obj, &TEST_DATA, false, CHECK_TIMEOUT_DURATION).await {
|
match Self::perform_health_check(
|
||||||
|
disk.clone(),
|
||||||
|
&TEST_BUCKET,
|
||||||
|
&test_obj,
|
||||||
|
&TEST_DATA,
|
||||||
|
false,
|
||||||
|
get_drive_active_check_timeout(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let state_before = health.runtime_state();
|
let state_before = health.runtime_state();
|
||||||
let is_online = health.mark_recovery_success(&disk.endpoint(), "recovery_probe_success");
|
let is_online = health.mark_recovery_success(&disk.endpoint(), "recovery_probe_success");
|
||||||
@@ -707,7 +762,7 @@ impl LocalDiskWrapper {
|
|||||||
let result = operation().await;
|
let result = operation().await;
|
||||||
self.health.decrement_waiting();
|
self.health.decrement_waiting();
|
||||||
if result.is_ok() {
|
if result.is_ok() {
|
||||||
self.health.log_success();
|
self.health.record_operation_success(&self.endpoint(), "operation_success");
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -718,7 +773,7 @@ impl LocalDiskWrapper {
|
|||||||
Ok(operation_result) => {
|
Ok(operation_result) => {
|
||||||
// Log success and decrement waiting counter
|
// Log success and decrement waiting counter
|
||||||
if operation_result.is_ok() {
|
if operation_result.is_ok() {
|
||||||
self.health.log_success();
|
self.health.record_operation_success(&self.endpoint(), "operation_success");
|
||||||
}
|
}
|
||||||
self.health.decrement_waiting();
|
self.health.decrement_waiting();
|
||||||
operation_result
|
operation_result
|
||||||
@@ -919,7 +974,7 @@ impl DiskAPI for LocalDiskWrapper {
|
|||||||
let has_err = result.iter().any(|e| e.is_some());
|
let has_err = result.iter().any(|e| e.is_some());
|
||||||
if !has_err {
|
if !has_err {
|
||||||
// Log success and decrement waiting counter
|
// Log success and decrement waiting counter
|
||||||
self.health.log_success();
|
self.health.record_operation_success(&self.endpoint(), "operation_success");
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
@@ -1112,37 +1167,92 @@ mod tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn drive_active_check_interval_uses_default_when_unset() {
|
||||||
|
temp_env::with_var_unset(rustfs_config::ENV_DRIVE_ACTIVE_CHECK_INTERVAL_SECS, || {
|
||||||
|
assert_eq!(
|
||||||
|
get_drive_active_check_interval(),
|
||||||
|
Duration::from_secs(rustfs_config::DEFAULT_DRIVE_ACTIVE_CHECK_INTERVAL_SECS)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn drive_active_check_interval_reads_env_override() {
|
||||||
|
temp_env::with_var(rustfs_config::ENV_DRIVE_ACTIVE_CHECK_INTERVAL_SECS, Some("3"), || {
|
||||||
|
assert_eq!(get_drive_active_check_interval(), Duration::from_secs(3));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn drive_active_check_timeout_uses_default_when_unset() {
|
||||||
|
temp_env::with_var_unset(rustfs_config::ENV_DRIVE_ACTIVE_CHECK_TIMEOUT_SECS, || {
|
||||||
|
assert_eq!(
|
||||||
|
get_drive_active_check_timeout(),
|
||||||
|
Duration::from_secs(rustfs_config::DEFAULT_DRIVE_ACTIVE_CHECK_TIMEOUT_SECS)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn drive_active_check_timeout_reads_env_override() {
|
||||||
|
temp_env::with_var(rustfs_config::ENV_DRIVE_ACTIVE_CHECK_TIMEOUT_SECS, Some("1"), || {
|
||||||
|
assert_eq!(get_drive_active_check_timeout(), Duration::from_secs(1));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_state_transitions_from_online_to_suspect_then_offline() {
|
fn runtime_state_transitions_from_online_to_suspect_then_offline() {
|
||||||
let endpoint = Endpoint::try_from("/tmp/runtime-state-disk").expect("endpoint should parse");
|
temp_env::with_var(rustfs_config::ENV_DRIVE_SUSPECT_FAILURE_THRESHOLD, Some("2"), || {
|
||||||
let health = DiskHealthTracker::new();
|
let endpoint = Endpoint::try_from("/tmp/runtime-state-disk").expect("endpoint should parse");
|
||||||
|
let health = DiskHealthTracker::new();
|
||||||
|
|
||||||
assert_eq!(health.runtime_state(), RuntimeDriveHealthState::Online);
|
assert_eq!(health.runtime_state(), RuntimeDriveHealthState::Online);
|
||||||
assert!(health.mark_failure(&endpoint, "timeout"));
|
assert!(!health.mark_failure(&endpoint, "timeout"));
|
||||||
assert_eq!(health.runtime_state(), RuntimeDriveHealthState::Suspect);
|
assert_eq!(health.runtime_state(), RuntimeDriveHealthState::Suspect);
|
||||||
|
assert!(!health.is_faulty());
|
||||||
|
|
||||||
assert!(!health.mark_failure(&endpoint, "timeout"));
|
assert!(health.mark_failure(&endpoint, "timeout"));
|
||||||
assert_eq!(health.runtime_state(), RuntimeDriveHealthState::Offline);
|
assert_eq!(health.runtime_state(), RuntimeDriveHealthState::Offline);
|
||||||
assert!(health.offline_duration().is_some());
|
assert!(health.is_faulty());
|
||||||
|
assert!(health.offline_duration().is_some());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_state_transitions_back_online_after_recovery_threshold() {
|
fn runtime_state_transitions_back_online_after_recovery_threshold() {
|
||||||
let endpoint = Endpoint::try_from("/tmp/runtime-state-recovery").expect("endpoint should parse");
|
temp_env::with_var(rustfs_config::ENV_DRIVE_SUSPECT_FAILURE_THRESHOLD, Some("2"), || {
|
||||||
|
let endpoint = Endpoint::try_from("/tmp/runtime-state-recovery").expect("endpoint should parse");
|
||||||
|
let health = DiskHealthTracker::new();
|
||||||
|
|
||||||
|
health.mark_failure(&endpoint, "timeout");
|
||||||
|
health.mark_failure(&endpoint, "timeout");
|
||||||
|
assert_eq!(health.runtime_state(), RuntimeDriveHealthState::Offline);
|
||||||
|
|
||||||
|
assert!(!health.mark_recovery_success(&endpoint, "probe"));
|
||||||
|
assert_eq!(health.runtime_state(), RuntimeDriveHealthState::Returning);
|
||||||
|
|
||||||
|
assert!(!health.mark_recovery_success(&endpoint, "probe"));
|
||||||
|
assert_eq!(health.runtime_state(), RuntimeDriveHealthState::Returning);
|
||||||
|
|
||||||
|
assert!(health.mark_recovery_success(&endpoint, "probe"));
|
||||||
|
assert_eq!(health.runtime_state(), RuntimeDriveHealthState::Online);
|
||||||
|
assert!(health.offline_duration().is_none());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn operation_success_recovers_suspect_drive_without_faulting() {
|
||||||
|
let endpoint = Endpoint::try_from("/tmp/runtime-state-suspect-success").expect("endpoint should parse");
|
||||||
let health = DiskHealthTracker::new();
|
let health = DiskHealthTracker::new();
|
||||||
|
|
||||||
health.mark_failure(&endpoint, "timeout");
|
assert!(!health.mark_failure(&endpoint, "timeout"));
|
||||||
health.mark_failure(&endpoint, "timeout");
|
assert_eq!(health.runtime_state(), RuntimeDriveHealthState::Suspect);
|
||||||
assert_eq!(health.runtime_state(), RuntimeDriveHealthState::Offline);
|
assert!(!health.is_faulty());
|
||||||
|
|
||||||
assert!(!health.mark_recovery_success(&endpoint, "probe"));
|
health.record_operation_success(&endpoint, "operation_success");
|
||||||
assert_eq!(health.runtime_state(), RuntimeDriveHealthState::Returning);
|
|
||||||
|
|
||||||
assert!(!health.mark_recovery_success(&endpoint, "probe"));
|
|
||||||
assert_eq!(health.runtime_state(), RuntimeDriveHealthState::Returning);
|
|
||||||
|
|
||||||
assert!(health.mark_recovery_success(&endpoint, "probe"));
|
|
||||||
assert_eq!(health.runtime_state(), RuntimeDriveHealthState::Online);
|
assert_eq!(health.runtime_state(), RuntimeDriveHealthState::Online);
|
||||||
|
assert!(!health.is_faulty());
|
||||||
assert!(health.offline_duration().is_none());
|
assert!(health.offline_duration().is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ use crate::disk::{
|
|||||||
use crate::erasure_coding::bitrot_verify;
|
use crate::erasure_coding::bitrot_verify;
|
||||||
use crate::global::{GLOBAL_IsErasureSD, GLOBAL_RootDiskThreshold};
|
use crate::global::{GLOBAL_IsErasureSD, GLOBAL_RootDiskThreshold};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
use metrics::counter;
|
||||||
use parking_lot::RwLock as ParkingLotRwLock;
|
use parking_lot::RwLock as ParkingLotRwLock;
|
||||||
use rustfs_filemeta::{
|
use rustfs_filemeta::{
|
||||||
Cache, FileInfo, FileInfoOpts, FileMeta, MetaCacheEntry, MetacacheWriter, ObjectPartInfo, Opts, RawFileInfo, UpdateFn,
|
Cache, FileInfo, FileInfoOpts, FileMeta, MetaCacheEntry, MetacacheWriter, ObjectPartInfo, Opts, RawFileInfo, UpdateFn,
|
||||||
@@ -79,6 +80,238 @@ pub enum InternalBuf<'a> {
|
|||||||
Owned(Bytes),
|
Owned(Bytes),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FileCacheReclaimWriter {
|
||||||
|
inner: File,
|
||||||
|
reclaim_len: usize,
|
||||||
|
reclaim_on_shutdown: bool,
|
||||||
|
reclaimed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FileCacheReclaimReader {
|
||||||
|
inner: File,
|
||||||
|
reclaim_offset: u64,
|
||||||
|
reclaim_len: usize,
|
||||||
|
reclaim_on_drop: bool,
|
||||||
|
reclaimed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_file_cache_reclaim_success(kind: &'static str, reclaim_len: usize, started: std::time::Instant) {
|
||||||
|
counter!("rustfs_page_cache_reclaim_requests_total", "kind" => kind.to_string(), "result" => "ok".to_string()).increment(1);
|
||||||
|
counter!("rustfs_page_cache_reclaim_bytes_total", "kind" => kind.to_string()).increment(reclaim_len as u64);
|
||||||
|
metrics::histogram!("rustfs_page_cache_reclaim_duration_seconds", "kind" => kind.to_string())
|
||||||
|
.record(started.elapsed().as_secs_f64());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_file_cache_reclaim_error(kind: &'static str) {
|
||||||
|
counter!("rustfs_page_cache_reclaim_requests_total", "kind" => kind.to_string(), "result" => "err".to_string()).increment(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileCacheReclaimReader {
|
||||||
|
fn new(inner: File, reclaim_offset: u64, reclaim_len: usize, reclaim_on_drop: bool) -> Self {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
if reclaim_on_drop {
|
||||||
|
let _ = set_fd_nocache(&inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
reclaim_offset,
|
||||||
|
reclaim_len,
|
||||||
|
reclaim_on_drop,
|
||||||
|
reclaimed: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn reclaim_file_cache(&mut self) -> std::io::Result<()> {
|
||||||
|
use core::num::NonZeroU64;
|
||||||
|
use rustix::fs::{Advice, fadvise};
|
||||||
|
|
||||||
|
if !self.reclaim_on_drop || self.reclaimed || self.reclaim_len == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let started = std::time::Instant::now();
|
||||||
|
let reclaim_len =
|
||||||
|
NonZeroU64::new(self.reclaim_len as u64).expect("reclaim_len is guaranteed non-zero by the early return");
|
||||||
|
fadvise(&self.inner, self.reclaim_offset, Some(reclaim_len), Advice::DontNeed).map_err(std::io::Error::from)?;
|
||||||
|
|
||||||
|
self.reclaimed = true;
|
||||||
|
record_file_cache_reclaim_success("read", self.reclaim_len, started);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
fn reclaim_file_cache(&mut self) -> std::io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
fn set_fd_nocache(file: &File) -> std::io::Result<()> {
|
||||||
|
use std::os::fd::AsRawFd;
|
||||||
|
|
||||||
|
// SAFETY: `fcntl` is called on a valid file descriptor owned by `file`.
|
||||||
|
let ret = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_NOCACHE, 1) };
|
||||||
|
if ret == -1 {
|
||||||
|
return Err(std::io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
fn set_std_fd_nocache(file: &std::fs::File) -> std::io::Result<()> {
|
||||||
|
use std::os::fd::AsRawFd;
|
||||||
|
|
||||||
|
// SAFETY: `fcntl` is called on a valid file descriptor owned by `file`.
|
||||||
|
let ret = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_NOCACHE, 1) };
|
||||||
|
if ret == -1 {
|
||||||
|
return Err(std::io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for FileCacheReclaimReader {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Err(err) = self.reclaim_file_cache() {
|
||||||
|
record_file_cache_reclaim_error("read");
|
||||||
|
debug!(error = ?err, reclaim_offset = self.reclaim_offset, reclaim_len = self.reclaim_len, "failed to reclaim file cache after read");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl tokio::io::AsyncRead for FileCacheReclaimReader {
|
||||||
|
fn poll_read(
|
||||||
|
mut self: std::pin::Pin<&mut Self>,
|
||||||
|
cx: &mut std::task::Context<'_>,
|
||||||
|
buf: &mut tokio::io::ReadBuf<'_>,
|
||||||
|
) -> std::task::Poll<std::io::Result<()>> {
|
||||||
|
std::pin::Pin::new(&mut self.inner).poll_read(cx, buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileCacheReclaimWriter {
|
||||||
|
fn new(inner: File, reclaim_len: usize, reclaim_on_shutdown: bool) -> Self {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
if reclaim_on_shutdown {
|
||||||
|
let _ = set_fd_nocache(&inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
reclaim_len,
|
||||||
|
reclaim_on_shutdown,
|
||||||
|
reclaimed: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn reclaim_file_cache(&mut self) -> std::io::Result<()> {
|
||||||
|
use core::num::NonZeroU64;
|
||||||
|
use rustix::fs::{Advice, fadvise};
|
||||||
|
|
||||||
|
if !self.reclaim_on_shutdown || self.reclaimed || self.reclaim_len == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let started = std::time::Instant::now();
|
||||||
|
let reclaim_len =
|
||||||
|
NonZeroU64::new(self.reclaim_len as u64).expect("reclaim_len is guaranteed non-zero by the early return");
|
||||||
|
fadvise(&self.inner, 0, Some(reclaim_len), Advice::DontNeed).map_err(std::io::Error::from)?;
|
||||||
|
|
||||||
|
self.reclaimed = true;
|
||||||
|
record_file_cache_reclaim_success("write", self.reclaim_len, started);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
fn reclaim_file_cache(&mut self) -> std::io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncWrite for FileCacheReclaimWriter {
|
||||||
|
fn poll_write(
|
||||||
|
mut self: std::pin::Pin<&mut Self>,
|
||||||
|
cx: &mut std::task::Context<'_>,
|
||||||
|
buf: &[u8],
|
||||||
|
) -> std::task::Poll<std::io::Result<usize>> {
|
||||||
|
std::pin::Pin::new(&mut self.inner).poll_write(cx, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_flush(mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll<std::io::Result<()>> {
|
||||||
|
std::pin::Pin::new(&mut self.inner).poll_flush(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_shutdown(
|
||||||
|
mut self: std::pin::Pin<&mut Self>,
|
||||||
|
cx: &mut std::task::Context<'_>,
|
||||||
|
) -> std::task::Poll<std::io::Result<()>> {
|
||||||
|
match std::pin::Pin::new(&mut self.inner).poll_shutdown(cx) {
|
||||||
|
std::task::Poll::Ready(Ok(())) => {
|
||||||
|
if let Err(err) = self.reclaim_file_cache() {
|
||||||
|
record_file_cache_reclaim_error("write");
|
||||||
|
debug!(error = ?err, reclaim_len = self.reclaim_len, "failed to reclaim file cache after write");
|
||||||
|
}
|
||||||
|
std::task::Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
other => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_write_vectored(
|
||||||
|
mut self: std::pin::Pin<&mut Self>,
|
||||||
|
cx: &mut std::task::Context<'_>,
|
||||||
|
bufs: &[std::io::IoSlice<'_>],
|
||||||
|
) -> std::task::Poll<std::io::Result<usize>> {
|
||||||
|
std::pin::Pin::new(&mut self.inner).poll_write_vectored(cx, bufs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_write_vectored(&self) -> bool {
|
||||||
|
self.inner.is_write_vectored()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_reclaim_file_cache_after_write(file_size: i64) -> bool {
|
||||||
|
if file_size <= 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rustfs_utils::get_env_bool(
|
||||||
|
rustfs_config::ENV_OBJECT_FILE_CACHE_RECLAIM_WRITE_ENABLE,
|
||||||
|
rustfs_config::DEFAULT_OBJECT_FILE_CACHE_RECLAIM_WRITE_ENABLE,
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let threshold = rustfs_utils::get_env_usize(
|
||||||
|
rustfs_config::ENV_OBJECT_FILE_CACHE_RECLAIM_THRESHOLD,
|
||||||
|
rustfs_config::DEFAULT_OBJECT_FILE_CACHE_RECLAIM_THRESHOLD,
|
||||||
|
);
|
||||||
|
file_size as usize >= threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_reclaim_file_cache_after_read(length: usize) -> bool {
|
||||||
|
if length == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rustfs_utils::get_env_bool(
|
||||||
|
rustfs_config::ENV_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE,
|
||||||
|
rustfs_config::DEFAULT_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE,
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let threshold = rustfs_utils::get_env_usize(
|
||||||
|
rustfs_config::ENV_OBJECT_FILE_CACHE_RECLAIM_THRESHOLD,
|
||||||
|
rustfs_config::DEFAULT_OBJECT_FILE_CACHE_RECLAIM_THRESHOLD,
|
||||||
|
);
|
||||||
|
length >= threshold
|
||||||
|
}
|
||||||
|
|
||||||
pub struct LocalDisk {
|
pub struct LocalDisk {
|
||||||
pub root: PathBuf,
|
pub root: PathBuf,
|
||||||
pub format_path: PathBuf,
|
pub format_path: PathBuf,
|
||||||
@@ -176,7 +409,15 @@ impl LocalDisk {
|
|||||||
let root = root_clone.clone();
|
let root = root_clone.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
match get_disk_info(root.clone()).await {
|
match get_disk_info(root.clone()).await {
|
||||||
Ok((info, root)) => {
|
Ok((info, is_root_disk)) => {
|
||||||
|
let physical_device_ids = match rustfs_utils::os::get_physical_device_ids(root.to_string_lossy().as_ref())
|
||||||
|
{
|
||||||
|
Ok(ids) => ids,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(root = ?root, error = ?err, "failed to resolve physical device ids for disk root");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
let disk_info = DiskInfo {
|
let disk_info = DiskInfo {
|
||||||
total: info.total,
|
total: info.total,
|
||||||
free: info.free,
|
free: info.free,
|
||||||
@@ -186,7 +427,8 @@ impl LocalDisk {
|
|||||||
major: info.major,
|
major: info.major,
|
||||||
minor: info.minor,
|
minor: info.minor,
|
||||||
fs_type: info.fstype,
|
fs_type: info.fstype,
|
||||||
root_disk: root,
|
root_disk: is_root_disk,
|
||||||
|
physical_device_ids,
|
||||||
id: disk_id,
|
id: disk_id,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@@ -1838,8 +2080,9 @@ impl DiskAPI for LocalDisk {
|
|||||||
let f = super::fs::open_file(&file_path, O_CREATE | O_WRONLY)
|
let f = super::fs::open_file(&file_path, O_CREATE | O_WRONLY)
|
||||||
.await
|
.await
|
||||||
.map_err(to_file_error)?;
|
.map_err(to_file_error)?;
|
||||||
|
let reclaim_on_shutdown = should_reclaim_file_cache_after_write(_file_size);
|
||||||
|
|
||||||
Ok(Box::new(f))
|
Ok(Box::new(FileCacheReclaimWriter::new(f, _file_size.max(0) as usize, reclaim_on_shutdown)))
|
||||||
|
|
||||||
// Ok(())
|
// Ok(())
|
||||||
}
|
}
|
||||||
@@ -1911,7 +2154,8 @@ impl DiskAPI for LocalDisk {
|
|||||||
f.seek(SeekFrom::Start(offset as u64)).await?;
|
f.seek(SeekFrom::Start(offset as u64)).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Box::new(f))
|
let reclaim_on_drop = should_reclaim_file_cache_after_read(length);
|
||||||
|
Ok(Box::new(FileCacheReclaimReader::new(f, offset as u64, length, reclaim_on_drop)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Zero-copy file read using memory mapping (Unix) or efficient read (non-Unix).
|
/// Zero-copy file read using memory mapping (Unix) or efficient read (non-Unix).
|
||||||
@@ -1956,9 +2200,15 @@ impl DiskAPI for LocalDisk {
|
|||||||
use memmap2::MmapOptions;
|
use memmap2::MmapOptions;
|
||||||
let file_path_clone = file_path.clone();
|
let file_path_clone = file_path.clone();
|
||||||
|
|
||||||
|
let should_reclaim_after_read = should_reclaim_file_cache_after_read(length);
|
||||||
let bytes = tokio::task::spawn_blocking(move || {
|
let bytes = tokio::task::spawn_blocking(move || {
|
||||||
let file = std::fs::File::open(&file_path_clone).map_err(DiskError::from)?;
|
let file = std::fs::File::open(&file_path_clone).map_err(DiskError::from)?;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
if should_reclaim_after_read {
|
||||||
|
let _ = set_std_fd_nocache(&file);
|
||||||
|
}
|
||||||
|
|
||||||
// mmap offsets on Unix must be page-size aligned. Align the
|
// mmap offsets on Unix must be page-size aligned. Align the
|
||||||
// mapping down to the nearest page boundary, then slice out the
|
// mapping down to the nearest page boundary, then slice out the
|
||||||
// originally requested logical range.
|
// originally requested logical range.
|
||||||
@@ -1986,7 +2236,21 @@ impl DiskAPI for LocalDisk {
|
|||||||
let end = logical_offset
|
let end = logical_offset
|
||||||
.checked_add(length)
|
.checked_add(length)
|
||||||
.ok_or_else(|| DiskError::other("mmap slice length overflow"))?;
|
.ok_or_else(|| DiskError::other("mmap slice length overflow"))?;
|
||||||
Ok::<Bytes, DiskError>(Bytes::copy_from_slice(&mmap[logical_offset..end]))
|
let bytes = Bytes::copy_from_slice(&mmap[logical_offset..end]);
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
if should_reclaim_after_read {
|
||||||
|
use core::num::NonZeroU64;
|
||||||
|
use rustix::fs::{Advice, fadvise};
|
||||||
|
|
||||||
|
let reclaim_len =
|
||||||
|
NonZeroU64::new(map_len as u64).ok_or_else(|| DiskError::other("mmap reclaim length overflow"))?;
|
||||||
|
fadvise(&file, aligned_offset, Some(reclaim_len), Advice::DontNeed)
|
||||||
|
.map_err(std::io::Error::from)
|
||||||
|
.map_err(DiskError::from)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<Bytes, DiskError>(bytes)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(DiskError::from)??;
|
.map_err(DiskError::from)??;
|
||||||
@@ -2072,6 +2336,7 @@ impl DiskAPI for LocalDisk {
|
|||||||
|
|
||||||
let mut objs_returned = 0;
|
let mut objs_returned = 0;
|
||||||
|
|
||||||
|
let mut skip_current_dir_object = false;
|
||||||
if opts.base_dir.ends_with(SLASH_SEPARATOR) {
|
if opts.base_dir.ends_with(SLASH_SEPARATOR) {
|
||||||
if let Ok(data) = self
|
if let Ok(data) = self
|
||||||
.read_metadata(
|
.read_metadata(
|
||||||
@@ -2098,7 +2363,7 @@ impl DiskAPI for LocalDisk {
|
|||||||
if let Ok(meta) = tokio::fs::metadata(fpath).await
|
if let Ok(meta) = tokio::fs::metadata(fpath).await
|
||||||
&& meta.is_file()
|
&& meta.is_file()
|
||||||
{
|
{
|
||||||
return Err(DiskError::FileNotFound);
|
skip_current_dir_object = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2109,7 +2374,7 @@ impl DiskAPI for LocalDisk {
|
|||||||
&opts,
|
&opts,
|
||||||
&mut out,
|
&mut out,
|
||||||
&mut objs_returned,
|
&mut objs_returned,
|
||||||
false,
|
skip_current_dir_object,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -2400,7 +2665,7 @@ impl DiskAPI for LocalDisk {
|
|||||||
|
|
||||||
let mut xl_meta = FileMeta::load(buf.as_ref())?;
|
let mut xl_meta = FileMeta::load(buf.as_ref())?;
|
||||||
|
|
||||||
xl_meta.update_object_version(fi)?;
|
xl_meta.update_object_version_with_opts(fi, opts.replace_user_metadata)?;
|
||||||
|
|
||||||
let wbuf = xl_meta.marshal_msg()?;
|
let wbuf = xl_meta.marshal_msg()?;
|
||||||
|
|
||||||
@@ -3362,4 +3627,32 @@ mod test {
|
|||||||
assert_eq!(normalize_path_components("C:\\a\\..\\b"), PathBuf::from("C:\\b"));
|
assert_eq!(normalize_path_components("C:\\a\\..\\b"), PathBuf::from("C:\\b"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_reclaim_file_cache_after_write_respects_env_and_threshold() {
|
||||||
|
temp_env::with_var_unset(rustfs_config::ENV_OBJECT_FILE_CACHE_RECLAIM_WRITE_ENABLE, || {
|
||||||
|
assert!(!should_reclaim_file_cache_after_write(8 * 1024 * 1024));
|
||||||
|
});
|
||||||
|
|
||||||
|
temp_env::with_var(rustfs_config::ENV_OBJECT_FILE_CACHE_RECLAIM_WRITE_ENABLE, Some("true"), || {
|
||||||
|
temp_env::with_var(rustfs_config::ENV_OBJECT_FILE_CACHE_RECLAIM_THRESHOLD, Some("4194304"), || {
|
||||||
|
assert!(should_reclaim_file_cache_after_write(8 * 1024 * 1024));
|
||||||
|
assert!(!should_reclaim_file_cache_after_write(1024));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_reclaim_file_cache_after_read_respects_env_and_threshold() {
|
||||||
|
temp_env::with_var_unset(rustfs_config::ENV_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE, || {
|
||||||
|
assert!(!should_reclaim_file_cache_after_read(8 * 1024 * 1024));
|
||||||
|
});
|
||||||
|
|
||||||
|
temp_env::with_var(rustfs_config::ENV_OBJECT_FILE_CACHE_RECLAIM_READ_ENABLE, Some("true"), || {
|
||||||
|
temp_env::with_var(rustfs_config::ENV_OBJECT_FILE_CACHE_RECLAIM_THRESHOLD, Some("4194304"), || {
|
||||||
|
assert!(should_reclaim_file_cache_after_read(8 * 1024 * 1024));
|
||||||
|
assert!(!should_reclaim_file_cache_after_read(1024));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -559,6 +559,7 @@ pub struct CheckPartsResp {
|
|||||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||||
pub struct UpdateMetadataOpts {
|
pub struct UpdateMetadataOpts {
|
||||||
pub no_persistence: bool,
|
pub no_persistence: bool,
|
||||||
|
pub replace_user_metadata: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DiskLocation {
|
pub struct DiskLocation {
|
||||||
@@ -596,6 +597,8 @@ pub struct DiskInfo {
|
|||||||
pub scanning: bool,
|
pub scanning: bool,
|
||||||
pub endpoint: String,
|
pub endpoint: String,
|
||||||
pub mount_path: String,
|
pub mount_path: String,
|
||||||
|
/// Leaf physical block devices backing this mount path when available.
|
||||||
|
pub physical_device_ids: Vec<String>,
|
||||||
pub id: Option<Uuid>,
|
pub id: Option<Uuid>,
|
||||||
pub rotational: bool,
|
pub rotational: bool,
|
||||||
pub metrics: DiskMetrics,
|
pub metrics: DiskMetrics,
|
||||||
@@ -912,9 +915,13 @@ mod tests {
|
|||||||
/// Test UpdateMetadataOpts structure
|
/// Test UpdateMetadataOpts structure
|
||||||
#[test]
|
#[test]
|
||||||
fn test_update_metadata_opts() {
|
fn test_update_metadata_opts() {
|
||||||
let opts = UpdateMetadataOpts { no_persistence: true };
|
let opts = UpdateMetadataOpts {
|
||||||
|
no_persistence: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
assert!(opts.no_persistence);
|
assert!(opts.no_persistence);
|
||||||
|
assert!(!opts.replace_user_metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test DiskOption structure
|
/// Test DiskOption structure
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ use crate::{
|
|||||||
disks_layout::DisksLayout,
|
disks_layout::DisksLayout,
|
||||||
global::global_rustfs_port,
|
global::global_rustfs_port,
|
||||||
};
|
};
|
||||||
|
use rustfs_config::{DEFAULT_UNSAFE_BYPASS_DISK_CHECK, ENV_MINIO_CI, ENV_UNSAFE_BYPASS_DISK_CHECK};
|
||||||
use rustfs_utils::{XHost, check_local_server_addr, get_host_ip, is_local_host};
|
use rustfs_utils::{XHost, check_local_server_addr, get_host_ip, is_local_host};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet, hash_map::Entry},
|
collections::{BTreeMap, BTreeSet, HashMap, HashSet, hash_map::Entry},
|
||||||
io::{Error, Result},
|
io::{Error, ErrorKind, Result},
|
||||||
net::IpAddr,
|
net::IpAddr,
|
||||||
};
|
};
|
||||||
use tracing::{error, info, instrument, warn};
|
use tracing::{error, info, instrument, warn};
|
||||||
@@ -348,6 +349,8 @@ impl PoolEndpointList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validate_local_physical_disk_independence(pool_endpoint_list.as_ref())?;
|
||||||
|
|
||||||
let setup_type = match pool_endpoint_list.as_ref()[0].as_ref()[0].get_type() {
|
let setup_type = match pool_endpoint_list.as_ref()[0].as_ref()[0].get_type() {
|
||||||
EndpointType::Path => SetupType::Erasure,
|
EndpointType::Path => SetupType::Erasure,
|
||||||
EndpointType::Url => match unique_args.len() {
|
EndpointType::Url => match unique_args.len() {
|
||||||
@@ -645,12 +648,118 @@ impl EndpointServerPools {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_local_physical_disk_independence(pools: &[Endpoints]) -> Result<()> {
|
||||||
|
let mut local_paths = BTreeSet::new();
|
||||||
|
for endpoints in pools {
|
||||||
|
for endpoint in endpoints.as_ref() {
|
||||||
|
if endpoint.is_local {
|
||||||
|
local_paths.insert(endpoint.get_file_path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if local_paths.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let local_paths = local_paths.into_iter().collect::<Vec<_>>();
|
||||||
|
validate_local_cross_device_mounts(&local_paths)?;
|
||||||
|
|
||||||
|
if local_paths.len() <= 1 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compatibility behavior:
|
||||||
|
// - canonical key: RUSTFS_UNSAFE_BYPASS_DISK_CHECK
|
||||||
|
// - legacy CI alias: MINIO_CI
|
||||||
|
// If both are set, `get_env_bool_with_aliases` keeps canonical key precedence.
|
||||||
|
if rustfs_utils::get_env_bool_with_aliases(ENV_UNSAFE_BYPASS_DISK_CHECK, &[ENV_MINIO_CI], DEFAULT_UNSAFE_BYPASS_DISK_CHECK) {
|
||||||
|
warn!(
|
||||||
|
env = ENV_UNSAFE_BYPASS_DISK_CHECK,
|
||||||
|
local_paths = ?local_paths,
|
||||||
|
"Skipping local physical disk independence validation due to explicit environment override",
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut device_paths = BTreeMap::<String, BTreeSet<String>>::new();
|
||||||
|
let mut missing_paths = Vec::new();
|
||||||
|
|
||||||
|
for path in &local_paths {
|
||||||
|
let canonical = match rustfs_utils::canonicalize(path) {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(err) if err.kind() == ErrorKind::NotFound => {
|
||||||
|
missing_paths.push(path.clone());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
return Err(Error::other(format!(
|
||||||
|
"failed to resolve local endpoint path '{path}' for disk validation: {err}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let canonical_path = canonical.to_string_lossy().into_owned();
|
||||||
|
let device_ids = rustfs_utils::os::get_physical_device_ids(&canonical_path).map_err(|err| {
|
||||||
|
Error::other(format!("failed to inspect physical disk for local endpoint '{canonical_path}': {err}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for device_id in device_ids {
|
||||||
|
device_paths.entry(device_id).or_default().insert(canonical_path.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !missing_paths.is_empty() {
|
||||||
|
warn!(
|
||||||
|
missing_paths = ?missing_paths,
|
||||||
|
"Excluding non-existent local endpoint paths from physical disk independence validation during endpoint parsing",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let shared_devices = device_paths
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(device_id, paths)| {
|
||||||
|
if paths.len() <= 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((device_id, paths.into_iter().collect::<Vec<_>>()))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if shared_devices.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let details = shared_devices
|
||||||
|
.into_iter()
|
||||||
|
.map(|(device_id, paths)| format!("{device_id} => {}", paths.join(", ")))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("; ");
|
||||||
|
|
||||||
|
Err(Error::other(format!(
|
||||||
|
"local erasure endpoints must use distinct physical disks; detected shared devices [{details}]. \
|
||||||
|
Set {ENV_UNSAFE_BYPASS_DISK_CHECK}=true only for local testing or CI to bypass this safety check"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_local_cross_device_mounts(local_paths: &[String]) -> Result<()> {
|
||||||
|
rustfs_utils::os::check_cross_device_mounts(local_paths)
|
||||||
|
.map_err(|err| Error::other(format!("local endpoint cross-device mount validation failed: {err}")))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use rustfs_utils::must_get_local_ips;
|
use rustfs_utils::must_get_local_ips;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use serial_test::serial;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use temp_env::async_with_vars;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new_endpoints() {
|
fn test_new_endpoints() {
|
||||||
@@ -1412,4 +1521,69 @@ mod test {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[serial]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn reject_shared_local_physical_disks_by_default() {
|
||||||
|
async_with_vars([(ENV_UNSAFE_BYPASS_DISK_CHECK, None::<&str>), (ENV_MINIO_CI, None::<&str>)], async {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let disk1 = dir.path().join("disk1");
|
||||||
|
let disk2 = dir.path().join("disk2");
|
||||||
|
std::fs::create_dir_all(&disk1).unwrap();
|
||||||
|
std::fs::create_dir_all(&disk2).unwrap();
|
||||||
|
|
||||||
|
let args = vec![disk1.to_string_lossy().into_owned(), disk2.to_string_lossy().into_owned()];
|
||||||
|
let layout = DisksLayout::from_volumes(args.as_slice()).unwrap();
|
||||||
|
|
||||||
|
let err = EndpointServerPools::create_server_endpoints("0.0.0.0:9000", &layout)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
let err_text = err.to_string();
|
||||||
|
assert!(err_text.contains("distinct physical disks"), "unexpected error: {err_text}");
|
||||||
|
assert!(err_text.contains(ENV_UNSAFE_BYPASS_DISK_CHECK), "unexpected error: {err_text}");
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[serial]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn allow_shared_local_physical_disks_with_explicit_env_bypass() {
|
||||||
|
async_with_vars([(ENV_UNSAFE_BYPASS_DISK_CHECK, Some("true"))], async {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let disk1 = dir.path().join("disk1");
|
||||||
|
let disk2 = dir.path().join("disk2");
|
||||||
|
std::fs::create_dir_all(&disk1).unwrap();
|
||||||
|
std::fs::create_dir_all(&disk2).unwrap();
|
||||||
|
|
||||||
|
let args = vec![disk1.to_string_lossy().into_owned(), disk2.to_string_lossy().into_owned()];
|
||||||
|
let layout = DisksLayout::from_volumes(args.as_slice()).unwrap();
|
||||||
|
|
||||||
|
let ret = EndpointServerPools::create_server_endpoints("0.0.0.0:9000", &layout).await;
|
||||||
|
assert!(ret.is_ok(), "expected bypassed disk validation to succeed, got {ret:?}");
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[serial]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn allow_shared_local_physical_disks_with_minio_ci_alias() {
|
||||||
|
async_with_vars([(ENV_UNSAFE_BYPASS_DISK_CHECK, None::<&str>), (ENV_MINIO_CI, Some("1"))], async {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let disk1 = dir.path().join("disk1");
|
||||||
|
let disk2 = dir.path().join("disk2");
|
||||||
|
std::fs::create_dir_all(&disk1).unwrap();
|
||||||
|
std::fs::create_dir_all(&disk2).unwrap();
|
||||||
|
|
||||||
|
let args = vec![disk1.to_string_lossy().into_owned(), disk2.to_string_lossy().into_owned()];
|
||||||
|
let layout = DisksLayout::from_volumes(args.as_slice()).unwrap();
|
||||||
|
|
||||||
|
let ret = EndpointServerPools::create_server_endpoints("0.0.0.0:9000", &layout).await;
|
||||||
|
assert!(ret.is_ok(), "expected MINIO_CI alias to bypass disk validation, got {ret:?}");
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,8 +166,19 @@ async fn write_data_blocks<W>(
|
|||||||
where
|
where
|
||||||
W: tokio::io::AsyncWrite + Send + Sync + Unpin,
|
W: tokio::io::AsyncWrite + Send + Sync + Unpin,
|
||||||
{
|
{
|
||||||
if get_data_block_len(en_blocks, data_blocks) < length {
|
if en_blocks.len() < data_blocks {
|
||||||
error!("write_data_blocks get_data_block_len < length");
|
return Err(io::Error::new(ErrorKind::InvalidInput, "data block count exceeds available shards"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if length == 0 {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(required_len) = offset.checked_add(length) else {
|
||||||
|
return Err(io::Error::new(ErrorKind::InvalidInput, "offset + length overflows"));
|
||||||
|
};
|
||||||
|
if get_data_block_len(en_blocks, data_blocks) < required_len {
|
||||||
|
error!("write_data_blocks not enough data after offset");
|
||||||
return Err(io::Error::new(ErrorKind::UnexpectedEof, "Not enough data blocks to write"));
|
return Err(io::Error::new(ErrorKind::UnexpectedEof, "Not enough data blocks to write"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,29 +199,22 @@ where
|
|||||||
let block_slice = &block[offset..];
|
let block_slice = &block[offset..];
|
||||||
offset = 0;
|
offset = 0;
|
||||||
|
|
||||||
if write_left < block_slice.len() {
|
let write_len = write_left.min(block_slice.len());
|
||||||
writer.write_all(&block_slice[..write_left]).await.map_err(|e| {
|
writer.write_all(&block_slice[..write_len]).await.map_err(|e| {
|
||||||
error!("write_data_blocks write_all err: {}", e);
|
error!("write_data_blocks write_all err: {}", e);
|
||||||
e
|
|
||||||
})?;
|
|
||||||
|
|
||||||
total_written += write_left;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let n = block_slice.len();
|
|
||||||
|
|
||||||
writer.write_all(block_slice).await.map_err(|e| {
|
|
||||||
error!("write_data_blocks write_all2 err: {}", e);
|
|
||||||
e
|
e
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
write_left -= n;
|
total_written += write_len;
|
||||||
|
write_left -= write_len;
|
||||||
|
|
||||||
total_written += n;
|
if write_left == 0 {
|
||||||
|
return Ok(total_written);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(total_written)
|
error!("write_data_blocks loop exhausted with write_left>0");
|
||||||
|
Err(io::Error::new(ErrorKind::UnexpectedEof, "Not enough data blocks to write"))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Erasure {
|
impl Erasure {
|
||||||
@@ -323,6 +327,112 @@ mod tests {
|
|||||||
use rustfs_utils::HashAlgorithm;
|
use rustfs_utils::HashAlgorithm;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_write_data_blocks_writes_range_across_blocks() {
|
||||||
|
let blocks = vec![Some(vec![1, 2, 3, 4]), Some(vec![5, 6, 7]), Some(vec![8, 9])];
|
||||||
|
let mut out = Vec::new();
|
||||||
|
|
||||||
|
let written = write_data_blocks(&mut out, &blocks, 3, 2, 5).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(written, 5);
|
||||||
|
assert_eq!(out, vec![3, 4, 5, 6, 7]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_write_data_blocks_rejects_short_data_after_offset() {
|
||||||
|
let blocks = vec![Some(vec![1, 2, 3, 4]), Some(vec![5, 6, 7])];
|
||||||
|
let mut out = Vec::new();
|
||||||
|
|
||||||
|
let err = write_data_blocks(&mut out, &blocks, 2, 3, 5).await.unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(err.kind(), ErrorKind::UnexpectedEof);
|
||||||
|
assert!(out.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_write_data_blocks_rejects_invalid_data_block_count() {
|
||||||
|
let blocks = vec![Some(vec![1, 2, 3, 4])];
|
||||||
|
let mut out = Vec::new();
|
||||||
|
|
||||||
|
let err = write_data_blocks(&mut out, &blocks, 2, 0, 1).await.unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(err.kind(), ErrorKind::InvalidInput);
|
||||||
|
assert!(out.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Regression for upstream issue #2716: ranged GETs going through
|
||||||
|
/// `Erasure::decode` must return the requested byte range without
|
||||||
|
/// panicking or truncating, including when the range starts at a
|
||||||
|
/// non-zero offset and crosses EC block boundaries.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_erasure_decode_ranged_read_returns_correct_bytes() {
|
||||||
|
const DATA_SHARDS: usize = 4;
|
||||||
|
const PARITY_SHARDS: usize = 2;
|
||||||
|
const BLOCK_SIZE: usize = 64;
|
||||||
|
|
||||||
|
// 200 bytes spans 3 full blocks + 1 partial block, exercising
|
||||||
|
// the start/middle/end branches in `Erasure::decode`.
|
||||||
|
let total_data: Vec<u8> = (0..200u32).map(|i| i as u8).collect();
|
||||||
|
let total_len = total_data.len();
|
||||||
|
|
||||||
|
let erasure = Erasure::new(DATA_SHARDS, PARITY_SHARDS, BLOCK_SIZE);
|
||||||
|
let total_shards = DATA_SHARDS + PARITY_SHARDS;
|
||||||
|
let shard_size = erasure.shard_size();
|
||||||
|
let hash_algo = HashAlgorithm::HighwayHash256;
|
||||||
|
|
||||||
|
let mut shard_writers: Vec<BitrotWriter<Cursor<Vec<u8>>>> = (0..total_shards)
|
||||||
|
.map(|_| BitrotWriter::new(Cursor::new(Vec::new()), shard_size, hash_algo.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut offset = 0;
|
||||||
|
while offset < total_len {
|
||||||
|
let end = (offset + BLOCK_SIZE).min(total_len);
|
||||||
|
let shards = erasure.encode_data(&total_data[offset..end]).unwrap();
|
||||||
|
for (i, shard) in shards.iter().enumerate() {
|
||||||
|
shard_writers[i].write(shard).await.unwrap();
|
||||||
|
}
|
||||||
|
offset = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
let shard_bufs: Vec<Vec<u8>> = shard_writers.into_iter().map(|w| w.into_inner().into_inner()).collect();
|
||||||
|
|
||||||
|
// `Erasure::decode` does not seek the readers; the production caller
|
||||||
|
// (`create_bitrot_reader`) positions each reader at the shard byte
|
||||||
|
// offset corresponding to the request's start block. Mirror that here.
|
||||||
|
let hash_size = hash_algo.size();
|
||||||
|
let make_readers = |off: usize| -> Vec<Option<BitrotReader<Cursor<Vec<u8>>>>> {
|
||||||
|
let start_block = off / BLOCK_SIZE;
|
||||||
|
let cursor_pos = start_block * (shard_size + hash_size);
|
||||||
|
shard_bufs
|
||||||
|
.iter()
|
||||||
|
.map(|buf| {
|
||||||
|
let mut cursor = Cursor::new(buf.clone());
|
||||||
|
cursor.set_position(cursor_pos as u64);
|
||||||
|
Some(BitrotReader::new(cursor, shard_size, hash_algo.clone(), false))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
// (offset, length, description)
|
||||||
|
let cases: &[(usize, usize, &str)] = &[
|
||||||
|
(0, total_len, "full read"),
|
||||||
|
(0, 50, "head from start, partial block"),
|
||||||
|
(10, 30, "small range within first block"),
|
||||||
|
(60, 80, "range crossing two block boundaries"),
|
||||||
|
(128, 50, "range starting at block boundary"),
|
||||||
|
(130, 10, "small range deep in middle"),
|
||||||
|
(192, 8, "tail covering last partial block"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for &(off, len, desc) in cases {
|
||||||
|
let mut output = Vec::new();
|
||||||
|
let (written, err) = erasure.decode(&mut output, make_readers(off), off, len, total_len).await;
|
||||||
|
assert!(err.is_none(), "{}: unexpected error: {:?}", desc, err);
|
||||||
|
assert_eq!(written, len, "{}: written != length", desc);
|
||||||
|
assert_eq!(output, total_data[off..off + len], "{}: bytes mismatch", desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_parallel_reader_normal() {
|
async fn test_parallel_reader_normal() {
|
||||||
const BLOCK_SIZE: usize = 64;
|
const BLOCK_SIZE: usize = 64;
|
||||||
|
|||||||
@@ -26,6 +26,30 @@ use tokio::io::AsyncRead;
|
|||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
|
const ENV_RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES: &str = "RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES";
|
||||||
|
const DEFAULT_RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES: usize = 8 * 1024 * 1024;
|
||||||
|
const DEFAULT_RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BLOCKS: usize = 8;
|
||||||
|
|
||||||
|
fn encode_channel_capacity(expanded_block_bytes: usize, max_inflight_bytes: usize) -> usize {
|
||||||
|
if expanded_block_bytes == 0 {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
max_inflight_bytes
|
||||||
|
.saturating_div(expanded_block_bytes)
|
||||||
|
.clamp(1, DEFAULT_RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BLOCKS)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn queued_block_bytes(block: &[Bytes]) -> usize {
|
||||||
|
block.iter().map(Bytes::len).sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn drain_queued_inflight_bytes(rx: &mut mpsc::Receiver<Vec<Bytes>>) {
|
||||||
|
while let Some(block) = rx.recv().await {
|
||||||
|
rustfs_io_metrics::remove_ec_encode_inflight_bytes(queued_block_bytes(&block));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct MultiWriter<'a> {
|
pub(crate) struct MultiWriter<'a> {
|
||||||
writers: &'a mut [Option<BitrotWriterWrapper>],
|
writers: &'a mut [Option<BitrotWriterWrapper>],
|
||||||
write_quorum: usize,
|
write_quorum: usize,
|
||||||
@@ -185,7 +209,14 @@ impl Erasure {
|
|||||||
where
|
where
|
||||||
R: AsyncRead + Send + Sync + Unpin + 'static,
|
R: AsyncRead + Send + Sync + Unpin + 'static,
|
||||||
{
|
{
|
||||||
let (tx, mut rx) = mpsc::channel::<Vec<Bytes>>(8);
|
// Bound queued encoded blocks by memory budget to avoid per-request spikes.
|
||||||
|
let expanded_block_bytes = self.shard_size().saturating_mul(self.total_shard_count());
|
||||||
|
let max_inflight_bytes = rustfs_utils::get_env_usize(
|
||||||
|
ENV_RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES,
|
||||||
|
DEFAULT_RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES,
|
||||||
|
);
|
||||||
|
let inflight_blocks = encode_channel_capacity(expanded_block_bytes, max_inflight_bytes);
|
||||||
|
let (tx, mut rx) = mpsc::channel::<Vec<Bytes>>(inflight_blocks);
|
||||||
|
|
||||||
let task = tokio::spawn(async move {
|
let task = tokio::spawn(async move {
|
||||||
let block_size = self.block_size;
|
let block_size = self.block_size;
|
||||||
@@ -196,7 +227,10 @@ impl Erasure {
|
|||||||
Ok(n) if n > 0 => {
|
Ok(n) if n > 0 => {
|
||||||
total += n;
|
total += n;
|
||||||
let res = self.encode_data(&buf[..n])?;
|
let res = self.encode_data(&buf[..n])?;
|
||||||
|
let queued_bytes = queued_block_bytes(&res);
|
||||||
|
rustfs_io_metrics::add_ec_encode_inflight_bytes(queued_bytes);
|
||||||
if let Err(err) = tx.send(res).await {
|
if let Err(err) = tx.send(res).await {
|
||||||
|
rustfs_io_metrics::remove_ec_encode_inflight_bytes(queued_bytes);
|
||||||
return Err(std::io::Error::other(format!("Failed to send encoded data : {err}")));
|
return Err(std::io::Error::other(format!("Failed to send encoded data : {err}")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,6 +263,8 @@ impl Erasure {
|
|||||||
if block.is_empty() {
|
if block.is_empty() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
let queued_bytes = queued_block_bytes(&block);
|
||||||
|
rustfs_io_metrics::remove_ec_encode_inflight_bytes(queued_bytes);
|
||||||
if let Err(err) = writers.write(block).await {
|
if let Err(err) = writers.write(block).await {
|
||||||
write_err = Some(err);
|
write_err = Some(err);
|
||||||
break;
|
break;
|
||||||
@@ -238,6 +274,7 @@ impl Erasure {
|
|||||||
if let Some(err) = write_err {
|
if let Some(err) = write_err {
|
||||||
task.abort();
|
task.abort();
|
||||||
let _ = task.await;
|
let _ = task.await;
|
||||||
|
drain_queued_inflight_bytes(&mut rx).await;
|
||||||
if let Err(shutdown_err) = writers.shutdown().await {
|
if let Err(shutdown_err) = writers.shutdown().await {
|
||||||
error!("failed to shutdown erasure writers after write error: {:?}", shutdown_err);
|
error!("failed to shutdown erasure writers after write error: {:?}", shutdown_err);
|
||||||
}
|
}
|
||||||
@@ -310,4 +347,18 @@ mod tests {
|
|||||||
assert_eq!(written, b"small payload".len());
|
assert_eq!(written, b"small payload".len());
|
||||||
assert!(!committed.lock().unwrap().is_empty());
|
assert!(!committed.lock().unwrap().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encode_channel_capacity_never_returns_zero() {
|
||||||
|
assert_eq!(encode_channel_capacity(0, 1024), 1);
|
||||||
|
assert_eq!(encode_channel_capacity(4096, 0), 1);
|
||||||
|
assert_eq!(encode_channel_capacity(4096, 1024), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encode_channel_capacity_respects_budget_and_hard_cap() {
|
||||||
|
assert_eq!(encode_channel_capacity(4 * 1024 * 1024, 32 * 1024 * 1024), 8);
|
||||||
|
assert_eq!(encode_channel_capacity(16 * 1024 * 1024, 32 * 1024 * 1024), 2);
|
||||||
|
assert_eq!(encode_channel_capacity(1, usize::MAX), DEFAULT_RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BLOCKS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use crate::rpc::PeerRestClient;
|
|||||||
use crate::{endpoints::EndpointServerPools, new_object_layer_fn};
|
use crate::{endpoints::EndpointServerPools, new_object_layer_fn};
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use rustfs_madmin::health::{Cpus, MemInfo, OsInfo, Partitions, ProcInfo, SysConfig, SysErrors, SysService};
|
use rustfs_madmin::health::{Cpus, MemInfo, OsInfo, Partitions, ProcInfo, SysConfig, SysErrors, SysServices};
|
||||||
use rustfs_madmin::metrics::RealtimeMetrics;
|
use rustfs_madmin::metrics::RealtimeMetrics;
|
||||||
use rustfs_madmin::net::NetInfo;
|
use rustfs_madmin::net::NetInfo;
|
||||||
use rustfs_madmin::{ItemState, ServerProperties};
|
use rustfs_madmin::{ItemState, ServerProperties};
|
||||||
@@ -485,31 +485,29 @@ impl NotificationSys {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn load_bucket_metadata(&self, bucket: &str) -> Vec<NotificationPeerErr> {
|
pub async fn load_bucket_metadata(&self, bucket: &str) -> Result<()> {
|
||||||
|
let operation = format!("load_bucket_metadata({bucket})");
|
||||||
|
let mut failures = Vec::new();
|
||||||
let mut futures = Vec::with_capacity(self.peer_clients.len());
|
let mut futures = Vec::with_capacity(self.peer_clients.len());
|
||||||
for client in self.peer_clients.iter() {
|
for (idx, client) in self.peer_clients.iter().enumerate() {
|
||||||
let b = bucket.to_string();
|
if let Some(client) = client {
|
||||||
futures.push(async move {
|
let host = client.host.to_string();
|
||||||
if let Some(client) = client {
|
let b = bucket.to_string();
|
||||||
match client.load_bucket_metadata(&b).await {
|
futures.push(async move { client.load_bucket_metadata(&b).await.map_err(|err| (host, err)) });
|
||||||
Ok(_) => NotificationPeerErr {
|
} else {
|
||||||
host: client.host.to_string(),
|
failures.push(format!("peer[{idx}] {operation} failed: peer is not reachable"));
|
||||||
err: None,
|
}
|
||||||
},
|
|
||||||
Err(e) => NotificationPeerErr {
|
|
||||||
host: client.host.to_string(),
|
|
||||||
err: Some(e),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NotificationPeerErr {
|
|
||||||
host: "".to_string(),
|
|
||||||
err: Some(Error::other("peer is not reachable")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
join_all(futures).await
|
|
||||||
|
for result in join_all(futures).await {
|
||||||
|
if let Err((host, err)) = result {
|
||||||
|
let failure = format!("peer {host} {operation} failed: {err}");
|
||||||
|
error!("notification {operation} err {failure}");
|
||||||
|
failures.push(failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aggregate_notification_failures(&operation, failures)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_bucket_metadata(&self, bucket: &str) -> Vec<NotificationPeerErr> {
|
pub async fn delete_bucket_metadata(&self, bucket: &str) -> Vec<NotificationPeerErr> {
|
||||||
@@ -622,14 +620,14 @@ impl NotificationSys {
|
|||||||
join_all(futures).await
|
join_all(futures).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_sys_services(&self) -> Vec<SysService> {
|
pub async fn get_sys_services(&self) -> Vec<SysServices> {
|
||||||
let mut futures = Vec::with_capacity(self.peer_clients.len());
|
let mut futures = Vec::with_capacity(self.peer_clients.len());
|
||||||
for client in self.peer_clients.iter().cloned() {
|
for client in self.peer_clients.iter().cloned() {
|
||||||
futures.push(async move {
|
futures.push(async move {
|
||||||
if let Some(client) = client {
|
if let Some(client) = client {
|
||||||
client.get_se_linux_info().await.unwrap_or_default()
|
client.get_se_linux_info().await.unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
SysService::default()
|
SysServices::default()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -904,4 +902,22 @@ mod tests {
|
|||||||
assert!(msg.contains("peer-1 failed"));
|
assert!(msg.contains("peer-1 failed"));
|
||||||
assert!(msg.contains("local save failed"));
|
assert!(msg.contains("local save failed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn load_bucket_metadata_reports_unreachable_peers() {
|
||||||
|
let sys = NotificationSys {
|
||||||
|
peer_clients: vec![None],
|
||||||
|
all_peer_clients: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let err = sys
|
||||||
|
.load_bucket_metadata("bucket-a")
|
||||||
|
.await
|
||||||
|
.expect_err("unreachable peers should fail bucket metadata reload");
|
||||||
|
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(msg.contains("load_bucket_metadata(bucket-a)"));
|
||||||
|
assert!(msg.contains("1 failure(s)"));
|
||||||
|
assert!(msg.contains("peer[0]"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-17
@@ -52,7 +52,7 @@ use rmp_serde::Serializer;
|
|||||||
use rustfs_common::defer;
|
use rustfs_common::defer;
|
||||||
use rustfs_common::heal_channel::HealOpts;
|
use rustfs_common::heal_channel::HealOpts;
|
||||||
use rustfs_filemeta::{FileInfoVersions, MetaCacheEntries, MetaCacheEntry, MetadataResolutionParams};
|
use rustfs_filemeta::{FileInfoVersions, MetaCacheEntries, MetaCacheEntry, MetadataResolutionParams};
|
||||||
use rustfs_utils::path::{SLASH_SEPARATOR, encode_dir_object, path_join};
|
use rustfs_utils::path::{encode_dir_object, path_join, path_to_bucket_object, path_to_bucket_object_with_base_path};
|
||||||
use rustfs_workers::workers::Workers;
|
use rustfs_workers::workers::Workers;
|
||||||
use s3s::dto::{BucketLifecycleConfiguration, DefaultRetention, ReplicationConfiguration};
|
use s3s::dto::{BucketLifecycleConfiguration, DefaultRetention, ReplicationConfiguration};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -986,25 +986,11 @@ impl PoolMeta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn path2_bucket_object(name: &str) -> (String, String) {
|
pub fn path2_bucket_object(name: &str) -> (String, String) {
|
||||||
path2_bucket_object_with_base_path("", name)
|
path_to_bucket_object(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path2_bucket_object_with_base_path(base_path: &str, path: &str) -> (String, String) {
|
pub fn path2_bucket_object_with_base_path(base_path: &str, path: &str) -> (String, String) {
|
||||||
// Trim the base path and leading slash
|
path_to_bucket_object_with_base_path(base_path, path)
|
||||||
let trimmed_path = path
|
|
||||||
.strip_prefix(base_path)
|
|
||||||
.unwrap_or(path)
|
|
||||||
.strip_prefix(SLASH_SEPARATOR)
|
|
||||||
.unwrap_or(path);
|
|
||||||
// Find the position of the first '/'
|
|
||||||
let Some(pos) = trimmed_path.find(SLASH_SEPARATOR) else {
|
|
||||||
return (trimmed_path.to_string(), "".to_string());
|
|
||||||
};
|
|
||||||
// Split into bucket and prefix
|
|
||||||
let bucket = &trimmed_path[0..pos];
|
|
||||||
let prefix = &trimmed_path[pos + 1..]; // +1 to skip the '/' character if it exists
|
|
||||||
|
|
||||||
(bucket.to_string(), prefix.to_string())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
@@ -3746,4 +3732,13 @@ mod pools_tests {
|
|||||||
.contains("failed to start decommission routines: scheduled 1 of 2 expected workers")
|
.contains("failed to start decommission routines: scheduled 1 of 2 expected workers")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn test_path2_bucket_object_with_base_path_supports_windows_separators() {
|
||||||
|
let (bucket, object) = super::path2_bucket_object_with_base_path("C:\\data", "C:\\data\\my-bucket\\nested\\object.txt");
|
||||||
|
|
||||||
|
assert_eq!(bucket, "my-bucket");
|
||||||
|
assert_eq!(object, "nested/object.txt");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,14 +12,17 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
use crate::disk::error::{DiskError, Error as DiskErrorType};
|
||||||
use crate::rpc::{TONIC_RPC_PREFIX, gen_signature_headers};
|
use crate::rpc::{TONIC_RPC_PREFIX, gen_signature_headers};
|
||||||
use http::Method;
|
use http::Method;
|
||||||
use rustfs_common::GLOBAL_CONN_MAP;
|
use rustfs_common::GLOBAL_CONN_MAP;
|
||||||
use rustfs_protos::{create_new_channel, proto_gen::node_service::node_service_client::NodeServiceClient};
|
use rustfs_protos::{create_new_channel, proto_gen::node_service::node_service_client::NodeServiceClient};
|
||||||
use std::error::Error;
|
use std::{error::Error, io::ErrorKind};
|
||||||
use tonic::{service::interceptor::InterceptedService, transport::Channel};
|
use tonic::{service::interceptor::InterceptedService, transport::Channel};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
|
use super::context_propagation::{inject_request_id_into_metadata, inject_trace_context_into_metadata};
|
||||||
|
|
||||||
/// 3. Subsequent calls will attempt fresh connections
|
/// 3. Subsequent calls will attempt fresh connections
|
||||||
/// 4. If node is still down, connection will fail fast (3s timeout)
|
/// 4. If node is still down, connection will fail fast (3s timeout)
|
||||||
pub async fn node_service_time_out_client(
|
pub async fn node_service_time_out_client(
|
||||||
@@ -49,12 +52,55 @@ pub async fn node_service_time_out_client_no_auth(
|
|||||||
node_service_time_out_client(addr, TonicInterceptor::NoOp(NoOpInterceptor)).await
|
node_service_time_out_client(addr, TonicInterceptor::NoOp(NoOpInterceptor)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_network_like_disk_error(err: &DiskErrorType) -> bool {
|
||||||
|
match err {
|
||||||
|
DiskError::Timeout => true,
|
||||||
|
DiskError::Io(io_err) => {
|
||||||
|
if matches!(
|
||||||
|
io_err.kind(),
|
||||||
|
ErrorKind::TimedOut
|
||||||
|
| ErrorKind::ConnectionRefused
|
||||||
|
| ErrorKind::ConnectionReset
|
||||||
|
| ErrorKind::BrokenPipe
|
||||||
|
| ErrorKind::NotConnected
|
||||||
|
| ErrorKind::ConnectionAborted
|
||||||
|
| ErrorKind::UnexpectedEof
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = io_err.to_string().to_ascii_lowercase();
|
||||||
|
[
|
||||||
|
"transport error",
|
||||||
|
"unavailable",
|
||||||
|
"error trying to connect",
|
||||||
|
"connection refused",
|
||||||
|
"connection reset",
|
||||||
|
"broken pipe",
|
||||||
|
"not connected",
|
||||||
|
"unexpected eof",
|
||||||
|
"timed out",
|
||||||
|
"deadline has elapsed",
|
||||||
|
"connection closed",
|
||||||
|
"connection aborted",
|
||||||
|
"tcp connect error",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.any(|needle| message.contains(needle))
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct TonicSignatureInterceptor;
|
pub struct TonicSignatureInterceptor;
|
||||||
|
|
||||||
impl tonic::service::Interceptor for TonicSignatureInterceptor {
|
impl tonic::service::Interceptor for TonicSignatureInterceptor {
|
||||||
fn call(&mut self, mut req: tonic::Request<()>) -> Result<tonic::Request<()>, tonic::Status> {
|
fn call(&mut self, mut req: tonic::Request<()>) -> Result<tonic::Request<()>, tonic::Status> {
|
||||||
let headers = gen_signature_headers(TONIC_RPC_PREFIX, &Method::GET);
|
let headers = gen_signature_headers(TONIC_RPC_PREFIX, &Method::GET)
|
||||||
|
.map_err(|_| tonic::Status::unauthenticated("No valid auth token"))?;
|
||||||
req.metadata_mut().as_mut().extend(headers);
|
req.metadata_mut().as_mut().extend(headers);
|
||||||
|
inject_trace_context_into_metadata(req.metadata_mut());
|
||||||
|
inject_request_id_into_metadata(req.metadata_mut());
|
||||||
Ok(req)
|
Ok(req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,3 +130,40 @@ impl tonic::service::Interceptor for TonicInterceptor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tonic::service::Interceptor;
|
||||||
|
|
||||||
|
fn ensure_test_rpc_secret() {
|
||||||
|
let _ = rustfs_credentials::GLOBAL_RUSTFS_RPC_SECRET.set("test-rpc-secret".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_signature_interceptor_keeps_auth_headers() {
|
||||||
|
ensure_test_rpc_secret();
|
||||||
|
let mut interceptor = TonicSignatureInterceptor;
|
||||||
|
let req = tonic::Request::new(());
|
||||||
|
|
||||||
|
let req = interceptor.call(req).expect("interceptor call should succeed");
|
||||||
|
|
||||||
|
assert!(req.metadata().contains_key("x-rustfs-signature"));
|
||||||
|
assert!(req.metadata().contains_key("x-rustfs-timestamp"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_signature_interceptor_may_inject_request_id() {
|
||||||
|
ensure_test_rpc_secret();
|
||||||
|
let mut interceptor = TonicSignatureInterceptor;
|
||||||
|
let req = tonic::Request::new(());
|
||||||
|
|
||||||
|
let span = tracing::info_span!("grpc-rpc-test-span");
|
||||||
|
let _guard = span.enter();
|
||||||
|
let req = interceptor.call(req).expect("interceptor call should succeed");
|
||||||
|
|
||||||
|
if let Some(v) = req.metadata().get("x-request-id") {
|
||||||
|
assert!(!v.as_encoded_bytes().is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
// Copyright 2024 RustFS Team
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
use http::{HeaderMap, HeaderValue};
|
||||||
|
use opentelemetry::{global, propagation::Injector, trace::TraceContextExt};
|
||||||
|
use tracing::Span;
|
||||||
|
use tracing_opentelemetry::OpenTelemetrySpanExt;
|
||||||
|
|
||||||
|
pub(crate) const REQUEST_ID_HEADER: &str = "x-request-id";
|
||||||
|
|
||||||
|
struct HttpHeaderInjector<'a> {
|
||||||
|
headers: &'a mut HeaderMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Injector for HttpHeaderInjector<'_> {
|
||||||
|
fn set(&mut self, key: &str, value: String) {
|
||||||
|
let Ok(name) = http::header::HeaderName::from_bytes(key.as_bytes()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(val) = HeaderValue::from_str(&value) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.headers.insert(name, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MetadataInjector<'a> {
|
||||||
|
metadata: &'a mut tonic::metadata::MetadataMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Injector for MetadataInjector<'_> {
|
||||||
|
fn set(&mut self, key: &str, value: String) {
|
||||||
|
let Ok(meta_key) = tonic::metadata::MetadataKey::from_bytes(key.as_bytes()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(meta_value) = tonic::metadata::MetadataValue::try_from(value.as_str()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.metadata.insert(meta_key, meta_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_trace_id() -> Option<String> {
|
||||||
|
let current_context = Span::current().context();
|
||||||
|
let current_span = current_context.span();
|
||||||
|
let span_context = current_span.span_context();
|
||||||
|
if !span_context.is_valid() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(span_context.trace_id().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fallback_request_id() -> String {
|
||||||
|
format!("req-{}", &uuid::Uuid::new_v4().to_string()[..8])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn propagated_request_id() -> String {
|
||||||
|
current_trace_id()
|
||||||
|
.map(|trace_id| format!("trace-{trace_id}"))
|
||||||
|
.unwrap_or_else(fallback_request_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inject_trace_context_into_http_headers(headers: &mut HeaderMap) {
|
||||||
|
let current_context = Span::current().context();
|
||||||
|
global::get_text_map_propagator(|propagator| {
|
||||||
|
let mut injector = HttpHeaderInjector { headers };
|
||||||
|
propagator.inject_context(¤t_context, &mut injector);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inject_request_id_into_http_headers(headers: &mut HeaderMap) {
|
||||||
|
if headers.contains_key(REQUEST_ID_HEADER) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let request_id = propagated_request_id();
|
||||||
|
if let Ok(value) = HeaderValue::from_str(&request_id) {
|
||||||
|
headers.insert(REQUEST_ID_HEADER, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inject_trace_context_into_metadata(metadata: &mut tonic::metadata::MetadataMap) {
|
||||||
|
let current_context = Span::current().context();
|
||||||
|
global::get_text_map_propagator(|propagator| {
|
||||||
|
let mut injector = MetadataInjector { metadata };
|
||||||
|
propagator.inject_context(¤t_context, &mut injector);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inject_request_id_into_metadata(metadata: &mut tonic::metadata::MetadataMap) {
|
||||||
|
let request_id_key = tonic::metadata::MetadataKey::from_static(REQUEST_ID_HEADER);
|
||||||
|
if metadata.contains_key(&request_id_key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let request_id = propagated_request_id();
|
||||||
|
let Ok(value) = tonic::metadata::MetadataValue::try_from(request_id.as_str()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
metadata.insert(request_id_key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use opentelemetry::trace::{SpanContext, TraceContextExt, TraceFlags, TraceId, TraceState, TracerProvider as _};
|
||||||
|
use opentelemetry_sdk::trace::SdkTracerProvider;
|
||||||
|
use tracing_opentelemetry::OpenTelemetrySpanExt;
|
||||||
|
use tracing_subscriber::{Registry, layer::SubscriberExt};
|
||||||
|
|
||||||
|
fn with_trace_parent<F>(trace_id_hex: &str, f: F)
|
||||||
|
where
|
||||||
|
F: FnOnce(),
|
||||||
|
{
|
||||||
|
let provider = SdkTracerProvider::builder().build();
|
||||||
|
let tracer = provider.tracer("context-propagation-tests");
|
||||||
|
let subscriber = Registry::default().with(tracing_opentelemetry::layer().with_tracer(tracer));
|
||||||
|
|
||||||
|
tracing::subscriber::with_default(subscriber, || {
|
||||||
|
let span = tracing::info_span!("context-propagation-test-span");
|
||||||
|
|
||||||
|
let trace_id = TraceId::from_hex(trace_id_hex).expect("trace id should be valid hex");
|
||||||
|
let span_id = opentelemetry::trace::SpanId::from_hex("0102030405060708").expect("span id should be valid hex");
|
||||||
|
let parent = SpanContext::new(trace_id, span_id, TraceFlags::SAMPLED, true, TraceState::default());
|
||||||
|
span.set_parent(opentelemetry::Context::new().with_remote_span_context(parent))
|
||||||
|
.expect("failed to set parent context");
|
||||||
|
let _guard = span.enter();
|
||||||
|
|
||||||
|
f();
|
||||||
|
});
|
||||||
|
let _ = provider.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inject_request_id_into_http_headers_preserves_existing_value() {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert(REQUEST_ID_HEADER, HeaderValue::from_static("req-upstream-123"));
|
||||||
|
|
||||||
|
with_trace_parent("0123456789abcdef0123456789abcdef", || {
|
||||||
|
inject_request_id_into_http_headers(&mut headers);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(headers.get(REQUEST_ID_HEADER).and_then(|v| v.to_str().ok()), Some("req-upstream-123"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inject_request_id_into_http_headers_uses_trace_id_when_missing() {
|
||||||
|
let trace_id = "abcdefabcdefabcdefabcdefabcdefab";
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
|
||||||
|
with_trace_parent(trace_id, || {
|
||||||
|
inject_request_id_into_http_headers(&mut headers);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
headers.get(REQUEST_ID_HEADER).and_then(|v| v.to_str().ok()),
|
||||||
|
Some(format!("trace-{trace_id}").as_str())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inject_request_id_into_metadata_preserves_existing_value() {
|
||||||
|
let mut metadata = tonic::metadata::MetadataMap::new();
|
||||||
|
metadata.insert(
|
||||||
|
tonic::metadata::MetadataKey::from_static(REQUEST_ID_HEADER),
|
||||||
|
tonic::metadata::MetadataValue::from_static("req-upstream-456"),
|
||||||
|
);
|
||||||
|
|
||||||
|
with_trace_parent("fedcba9876543210fedcba9876543210", || {
|
||||||
|
inject_request_id_into_metadata(&mut metadata);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(metadata.get(REQUEST_ID_HEADER).and_then(|v| v.to_str().ok()), Some("req-upstream-456"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inject_request_id_into_metadata_uses_trace_id_when_missing() {
|
||||||
|
let trace_id = "1234567890abcdef1234567890abcdef";
|
||||||
|
let mut metadata = tonic::metadata::MetadataMap::new();
|
||||||
|
|
||||||
|
with_trace_parent(trace_id, || {
|
||||||
|
inject_request_id_into_metadata(&mut metadata);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
metadata.get(REQUEST_ID_HEADER).and_then(|v| v.to_str().ok()),
|
||||||
|
Some(format!("trace-{trace_id}").as_str())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inject_request_id_into_http_headers_uses_req_fallback_when_trace_missing() {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
inject_request_id_into_http_headers(&mut headers);
|
||||||
|
|
||||||
|
let request_id = headers
|
||||||
|
.get(REQUEST_ID_HEADER)
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.expect("request id should be injected");
|
||||||
|
assert!(request_id.starts_with("req-"), "expected req- fallback, got: {request_id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_inject_request_id_into_metadata_uses_req_fallback_when_trace_missing() {
|
||||||
|
let mut metadata = tonic::metadata::MetadataMap::new();
|
||||||
|
inject_request_id_into_metadata(&mut metadata);
|
||||||
|
|
||||||
|
let request_id = metadata
|
||||||
|
.get(REQUEST_ID_HEADER)
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.expect("request id should be injected");
|
||||||
|
assert!(request_id.starts_with("req-"), "expected req- fallback, got: {request_id}");
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user