mirror of
https://github.com/rustfs/rustfs.git
synced 2026-11-08 19:43:05 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1dea00ffa7 |
@@ -1,2 +1,2 @@
|
||||
sha256-darwin=219ac11e720b3f54dfee4d5d5e95171785ba159b2fe01db3037a8f5061c1ad32
|
||||
sha256-linux=2d34698cc8ec52d5b6c37ac612919a1d21f36cd56a1af539a7522a4eea518e26
|
||||
sha256-darwin=9901a4950099b94d3d2618524d540f1ed4bf6023044903886f98b6c4bf5f30fb
|
||||
sha256-linux=09fd5f48f5556c9fc576357222dccd7ebb0b90b790798f1834db2ec5d67b4952
|
||||
|
||||
@@ -54,6 +54,7 @@ script-tests: ## Run shell script tests
|
||||
./scripts/check_embedded_secrets.sh --self-test
|
||||
$(RUSTFS_PYTHON_BIN) ./scripts/check_test_wiring.py --self-test
|
||||
$(RUSTFS_PYTHON_BIN) ./scripts/test_e2e_binary.py
|
||||
$(RUSTFS_PYTHON_BIN) ./scripts/test_download_release_asset.py
|
||||
$(RUSTFS_PYTHON_BIN) ./scripts/test_inspect_btf_prototypes.py
|
||||
$(RUSTFS_PYTHON_BIN) ./scripts/test_release_build_workflow.py
|
||||
$(RUSTFS_PYTHON_BIN) ./scripts/test_release_glibc.py
|
||||
|
||||
@@ -28,11 +28,12 @@ runs:
|
||||
|
||||
- name: Install actionlint
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
ASSET_DOWNLOADER: ${{ github.action_path }}/../setup/download_release_asset.py
|
||||
run: |
|
||||
actionlint_dir="$(mktemp -d "${RUNNER_TEMP}/actionlint.XXXXXX")"
|
||||
curl --fail --location --silent --show-error \
|
||||
--output "$actionlint_dir/actionlint.tar.gz" \
|
||||
https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz
|
||||
python3 "$ASSET_DOWNLOADER" --repository rhysd/actionlint --tag v1.7.12 --asset actionlint_1.7.12_linux_amd64.tar.gz --output "$actionlint_dir/actionlint.tar.gz" --sha256 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8
|
||||
echo "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 $actionlint_dir/actionlint.tar.gz" | sha256sum --check --status
|
||||
tar -xzf "$actionlint_dir/actionlint.tar.gz" -C "$actionlint_dir" actionlint
|
||||
rm "$actionlint_dir/actionlint.tar.gz"
|
||||
|
||||
@@ -66,10 +66,7 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
# protobuf-compiler is deliberately absent: the setup-protoc step below
|
||||
# installs 35.1 into the tool cache and prepends it to PATH, so the apt
|
||||
# build (older, and never version-matched) was shadowed on every run and
|
||||
# simply never used.
|
||||
# Use the pinned code generators rather than Ubuntu's older packages.
|
||||
- name: Install system dependencies (Ubuntu)
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
@@ -79,7 +76,9 @@ runs:
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
ripgrep
|
||||
ripgrep \
|
||||
python3 \
|
||||
gh
|
||||
|
||||
# musl-gcc is needed by the native musl release leg, and zip/unzip by the
|
||||
# release packaging steps. No CI test lane touches any of them.
|
||||
@@ -88,13 +87,32 @@ runs:
|
||||
shell: bash
|
||||
run: sudo apt-get install -y musl-tools zip unzip
|
||||
|
||||
- name: Install pinned code generators through GitHub API (Linux x64)
|
||||
if: runner.os == 'Linux' && runner.arch == 'X64'
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
ASSET_DOWNLOADER: ${{ github.action_path }}/download_release_asset.py
|
||||
run: |
|
||||
python3 "$ASSET_DOWNLOADER" --repository protocolbuffers/protobuf --tag v35.1 --asset protoc-35.1-linux-x86_64.zip --output "$RUNNER_TEMP/protoc.zip"
|
||||
python3 "$ASSET_DOWNLOADER" --repository google/flatbuffers --tag v25.12.19 --asset 'Linux.flatc.binary.g++-13.zip' --output "$RUNNER_TEMP/flatc.zip"
|
||||
python3 -m zipfile -e "$RUNNER_TEMP/protoc.zip" "$RUNNER_TEMP/protoc"
|
||||
python3 -m zipfile -e "$RUNNER_TEMP/flatc.zip" "$RUNNER_TEMP/flatc"
|
||||
chmod +x "$RUNNER_TEMP/protoc/bin/protoc" "$RUNNER_TEMP/flatc/flatc"
|
||||
"$RUNNER_TEMP/protoc/bin/protoc" --version
|
||||
"$RUNNER_TEMP/flatc/flatc" --version
|
||||
echo "$RUNNER_TEMP/protoc/bin" >> "$GITHUB_PATH"
|
||||
echo "$RUNNER_TEMP/flatc" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install protoc
|
||||
if: runner.os != 'Linux' || runner.arch != 'X64'
|
||||
uses: rustfs/setup-protoc@a3705324d8f9bf5b6c3573fb6cf8ae421db55dd6 # v3.0.1
|
||||
with:
|
||||
version: "35.1"
|
||||
repo-token: ${{ github.token }}
|
||||
|
||||
- name: Install flatc
|
||||
if: runner.os != 'Linux' || runner.arch != 'X64'
|
||||
uses: Nugine/setup-flatc@698800de72a96bfb22cf60431dc21a2ff9a7e07b # v1
|
||||
with:
|
||||
version: "25.12.19"
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Download a named GitHub release asset through its API and verify its digest."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
def download(repository, tag, name, output, expected_sha256=None):
|
||||
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*/[A-Za-z0-9][A-Za-z0-9_.-]*", repository):
|
||||
raise ValueError("invalid release repository")
|
||||
if not tag or not name or "/" in name or "\\" in name:
|
||||
raise ValueError("invalid release tag or asset name")
|
||||
if expected_sha256 is not None and not re.fullmatch(r"[0-9a-f]{64}", expected_sha256):
|
||||
raise ValueError("invalid pinned asset digest")
|
||||
result = subprocess.run(
|
||||
["gh", "api", "--hostname", "github.com", f"repos/{repository}/releases/tags/{quote(tag, safe='')}"],
|
||||
check=True, capture_output=True, text=True, timeout=60,
|
||||
)
|
||||
release = json.loads(result.stdout)
|
||||
if not isinstance(release, dict) or release.get("tag_name") != tag:
|
||||
raise ValueError("release metadata does not match the requested tag")
|
||||
assets = release.get("assets")
|
||||
if not isinstance(assets, list) or any(not isinstance(asset, dict) for asset in assets):
|
||||
raise ValueError("invalid release asset inventory")
|
||||
matches = [asset for asset in assets if asset.get("name") == name]
|
||||
if len(matches) != 1:
|
||||
raise ValueError("release must contain exactly one named asset")
|
||||
asset = matches[0]
|
||||
asset_id, size, digest = asset.get("id"), asset.get("size"), asset.get("digest")
|
||||
if type(asset_id) is not int or asset_id <= 0 or type(size) is not int or not 0 < size <= 512 * 1024 * 1024:
|
||||
raise ValueError("invalid release asset identity or size")
|
||||
if not isinstance(digest, str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", digest):
|
||||
raise ValueError("release asset has no valid SHA-256 digest")
|
||||
sha256 = digest.removeprefix("sha256:")
|
||||
if expected_sha256 is not None and expected_sha256 != sha256:
|
||||
raise ValueError("release digest differs from the pinned digest")
|
||||
output = Path(output)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(dir=output.parent, prefix=".release-asset-", delete=False) as stream:
|
||||
temporary = Path(stream.name)
|
||||
# gh authenticates to the API; its HTTP client strips credentials on
|
||||
# cross-host redirects. No browser URL or token is supplied in argv.
|
||||
subprocess.run(
|
||||
["gh", "api", "--hostname", "github.com", "--header", "Accept: application/octet-stream", f"repos/{repository}/releases/assets/{asset_id}"],
|
||||
check=True, stdout=stream, timeout=300,
|
||||
)
|
||||
with temporary.open("rb") as stream:
|
||||
actual = hashlib.file_digest(stream, "sha256").hexdigest()
|
||||
if temporary.stat().st_size != size or actual != sha256:
|
||||
raise ValueError("downloaded release asset failed size or SHA-256 verification")
|
||||
temporary.replace(output)
|
||||
print(f"Verified release asset: {repository} {tag} {name} sha256={actual}")
|
||||
finally:
|
||||
if temporary is not None:
|
||||
temporary.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--repository", required=True)
|
||||
parser.add_argument("--tag", required=True)
|
||||
parser.add_argument("--asset", required=True)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
parser.add_argument("--sha256")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
download(args.repository, args.tag, args.asset, args.output, args.sha256)
|
||||
except (ValueError, KeyError, TypeError, json.JSONDecodeError, subprocess.SubprocessError, OSError) as error:
|
||||
print(f"Release asset download failed: {error}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -142,15 +142,23 @@ jobs:
|
||||
- uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install pinned typos through GitHub API
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TYPOS_VERSION: 1.50.2
|
||||
run: |
|
||||
python3 .github/actions/setup/download_release_asset.py --repository crate-ci/typos --tag "v${TYPOS_VERSION}" --asset "typos-v${TYPOS_VERSION}-x86_64-unknown-linux-musl.tar.gz" --output "$RUNNER_TEMP/typos.tar.gz"
|
||||
mkdir -p "$RUNNER_TEMP/typos"
|
||||
tar -xzf "$RUNNER_TEMP/typos.tar.gz" -C "$RUNNER_TEMP/typos"
|
||||
- name: Typos check with custom config file
|
||||
uses: crate-ci/typos@512fc24f32f44ab01972217aaaf3dc86ec234d53 # v1.50.2
|
||||
run: '"$RUNNER_TEMP/typos/typos" .'
|
||||
|
||||
# Fail early with compile-free checks for every pull request.
|
||||
quick-checks:
|
||||
name: Quick Checks
|
||||
if: github.event_name != 'pull_request' || github.event.action != 'closed'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7
|
||||
@@ -1137,11 +1145,16 @@ jobs:
|
||||
|
||||
- name: Install mc
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
MC_VERSION: RELEASE.2025-08-13T08-35-41Z
|
||||
MC_SHA256: 01f866e9c5f9b87c2b09116fa5d7c06695b106242d829a8bb32990c00312e891
|
||||
run: |
|
||||
if ! command -v gh >/dev/null 2>&1; then
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gh
|
||||
fi
|
||||
MC_BINARY="mc.linux-amd64.${MC_VERSION}"
|
||||
curl -fsSLo "$RUNNER_TEMP/mc" "https://github.com/minio/mc/releases/download/${MC_VERSION}/${MC_BINARY}"
|
||||
python3 .github/actions/setup/download_release_asset.py --repository minio/mc --tag "$MC_VERSION" --asset "$MC_BINARY" --output "$RUNNER_TEMP/mc" --sha256 "$MC_SHA256"
|
||||
echo "${MC_SHA256} $RUNNER_TEMP/mc" | sha256sum --check --status
|
||||
chmod +x "$RUNNER_TEMP/mc"
|
||||
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
|
||||
|
||||
@@ -7,10 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Notifications and audit
|
||||
|
||||
- Notify and audit targets that keep the default `queue_dir` now work on native Windows: the defaults `/opt/rustfs/events` and `/opt/rustfs/audit` resolve per node, to `%ProgramData%\RustFS\events` and `%ProgramData%\RustFS\audit` on Windows, instead of failing validation as non-absolute paths. The new `RUSTFS_TARGET_STORE_DIR` (Helm `config.rustfs.target_store_dir`) relocates both default queues on any platform. Linux and macOS behavior is unchanged when it is unset, persisted configuration is unchanged, and pending entries are not migrated when the root changes. See `docs/operations/target-queue-storage.md`.
|
||||
|
||||
### Deployment
|
||||
|
||||
- Existing volumes created by older root-running container images must be writable by the non-root runtime identity before upgrading. The default container UID/GID is `10001:10001`; stop the affected instances, verify host mount paths, and for ordinary bind mounts run `sudo chown -R 10001:10001 /actual/data /actual/logs` on each RustFS data/log mount. Account for ACLs, NFS, Kubernetes, and rootless UID mappings. Image-build ownership changes do not migrate mounted data. See the Docker Quick Start in [README.md](README.md).
|
||||
|
||||
@@ -199,7 +199,7 @@ docker run -d --name rustfs -p 9000:9000 \
|
||||
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, the official image maps `/opt/rustfs/events` into the persistent `/data` volume so it is writable by the runtime user and pending events survive container recreation. Override `queue_dir` when using another deployment layout, or set `RUSTFS_TARGET_STORE_DIR` to move every default queue; native Windows defaults to `%ProgramData%\RustFS`. See [Target queue storage](docs/operations/target-queue-storage.md).
|
||||
- If queue dir is omitted, the official image maps `/opt/rustfs/events` into the persistent `/data` volume so it is writable by the runtime user and pending events survive container recreation. Override `queue_dir` when using another deployment layout.
|
||||
- `RUSTFS_NOTIFY_WEBHOOK_SKIP_TLS_VERIFY_PRIMARY` defaults to `false`; enabling it skips webhook TLS certificate verification, allows MITM attacks, and emits a startup warning. Prefer `RUSTFS_NOTIFY_WEBHOOK_CLIENT_CA_PRIMARY` for private CAs.
|
||||
- Since `1.0.0-beta.11`, webhook endpoints on private or container networks
|
||||
(`Docker Compose service names`, `host.docker.internal`, RFC 1918 addresses) are
|
||||
|
||||
@@ -276,13 +276,6 @@ Drive timeout profile preset:
|
||||
- values must be positive; `0` or an invalid value falls back to the default, and values above `60` are clamped to `60`.
|
||||
- the setting is read by the aggregating node only; it does not change the internode RPC wire contract. Any retry shares one round deadline rather than receiving a fresh timeout.
|
||||
|
||||
## Target queue directory
|
||||
|
||||
- `RUSTFS_TARGET_STORE_DIR`
|
||||
- absolute root for notify and audit target queues whose `queue_dir` is the default `/opt/rustfs/events` or `/opt/rustfs/audit`; they open `<root>/events` and `<root>/audit` on this node.
|
||||
- unset keeps those paths on Linux and macOS; native Windows uses `%ProgramData%\RustFS\events` and `%ProgramData%\RustFS\audit`.
|
||||
- explicit `queue_dir` values are never relocated, and the persisted target configuration is unchanged. Changing the root does not migrate pending entries. See `docs/operations/target-queue-storage.md`.
|
||||
|
||||
## Startup filesystem boundary policy
|
||||
|
||||
- `RUSTFS_UNSUPPORTED_FS_POLICY` controls startup behavior when RustFS detects local endpoint filesystems that are outside the supported production boundary.
|
||||
|
||||
@@ -144,12 +144,6 @@ pub const POSTGRES_QUEUE_LIMIT: &str = "queue_limit";
|
||||
/// Applies to both notify and audit target queue stores.
|
||||
pub const ENV_TARGET_STORE_COMPRESS: &str = "RUSTFS_TARGET_STORE_COMPRESS";
|
||||
|
||||
/// Environment variable that relocates the root of the default notify and audit queue directories.
|
||||
///
|
||||
/// It applies only to targets whose `queue_dir` is the logical default (`EVENT_DEFAULT_DIR` or
|
||||
/// `AUDIT_DEFAULT_DIR`); their queues move to `<root>/events` and `<root>/audit` on this node.
|
||||
pub const ENV_TARGET_STORE_DIR: &str = "RUSTFS_TARGET_STORE_DIR";
|
||||
|
||||
/// Queue-store compression is enabled by default to reduce disk footprint.
|
||||
pub const DEFAULT_TARGET_STORE_COMPRESS: bool = true;
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ pub(crate) fn notify_configuration_hint() -> String {
|
||||
format!(
|
||||
"No notify targets configured. Check {}=true and instance-scoped target env vars (for example {webhook_enable_primary} + {webhook_endpoint_primary} for arn:rustfs:sqs::primary:webhook). If using default queue_dir, ensure {} is writable.",
|
||||
rustfs_config::ENV_NOTIFY_ENABLE,
|
||||
rustfs_targets::store::resolve_queue_dir(rustfs_config::EVENT_DEFAULT_DIR),
|
||||
rustfs_config::EVENT_DEFAULT_DIR,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
use crate::TargetError;
|
||||
use crate::store::resolve_queue_dir;
|
||||
use crate::target::nats::validate_jetstream_settings;
|
||||
use crate::target::pulsar::validate_pulsar_broker;
|
||||
use async_nats::ServerAddr;
|
||||
@@ -30,12 +29,6 @@ use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use url::Url;
|
||||
|
||||
/// Return the queue directory this node opens for a target, with logical defaults resolved.
|
||||
pub(super) fn queue_dir_or_default(config: &KVS, key: &str, default_queue_dir: &str) -> String {
|
||||
let queue_dir = config.lookup(key).unwrap_or_else(|| default_queue_dir.to_string());
|
||||
resolve_queue_dir(&queue_dir).into_owned()
|
||||
}
|
||||
|
||||
pub(super) fn split_env_field_and_instance(rest: &str, valid_fields: &HashSet<String>) -> Option<(String, String)> {
|
||||
let normalized = rest.to_lowercase();
|
||||
if valid_fields.contains(&normalized) {
|
||||
@@ -131,7 +124,7 @@ pub(super) fn validate_nats_server_config(server: &ServerAddr, config: &KVS, def
|
||||
));
|
||||
}
|
||||
|
||||
let queue_dir = queue_dir_or_default(config, NATS_QUEUE_DIR, default_queue_dir);
|
||||
let queue_dir = config.lookup(NATS_QUEUE_DIR).unwrap_or_else(|| default_queue_dir.to_string());
|
||||
if !queue_dir.is_empty() && !Path::new(&queue_dir).is_absolute() {
|
||||
return Err(TargetError::Configuration("NATS queue directory must be an absolute path".to_string()));
|
||||
}
|
||||
@@ -211,7 +204,9 @@ pub(super) fn validate_pulsar_broker_config(broker: &str, config: &KVS, default_
|
||||
));
|
||||
}
|
||||
|
||||
let queue_dir = queue_dir_or_default(config, PULSAR_QUEUE_DIR, default_queue_dir);
|
||||
let queue_dir = config
|
||||
.lookup(PULSAR_QUEUE_DIR)
|
||||
.unwrap_or_else(|| default_queue_dir.to_string());
|
||||
if !queue_dir.is_empty() && !Path::new(&queue_dir).is_absolute() {
|
||||
return Err(TargetError::Configuration("Pulsar queue directory must be an absolute path".to_string()));
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
// limitations under the License.
|
||||
|
||||
use super::common::{
|
||||
parse_jetstream_ack_timeout_secs, parse_jetstream_enable, parse_target_bool, parse_url, queue_dir_or_default,
|
||||
validate_nats_server_config, validate_outbound_http_url, validate_pulsar_broker_config,
|
||||
parse_jetstream_ack_timeout_secs, parse_jetstream_enable, parse_target_bool, parse_url, validate_nats_server_config,
|
||||
validate_outbound_http_url, validate_pulsar_broker_config,
|
||||
};
|
||||
use crate::error::TargetError;
|
||||
use crate::target::{
|
||||
@@ -118,7 +118,7 @@ pub fn build_amqp_args(config: &KVS, default_queue_dir: &str, target_type: Targe
|
||||
tls_ca: config.lookup(AMQP_TLS_CA).unwrap_or_default(),
|
||||
tls_client_cert: config.lookup(AMQP_TLS_CLIENT_CERT).unwrap_or_default(),
|
||||
tls_client_key: config.lookup(AMQP_TLS_CLIENT_KEY).unwrap_or_default(),
|
||||
queue_dir: queue_dir_or_default(config, AMQP_QUEUE_DIR, default_queue_dir),
|
||||
queue_dir: config.lookup(AMQP_QUEUE_DIR).unwrap_or_else(|| default_queue_dir.to_string()),
|
||||
queue_limit: config
|
||||
.lookup(AMQP_QUEUE_LIMIT)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
@@ -146,7 +146,9 @@ pub fn build_webhook_args(config: &KVS, default_queue_dir: &str, target_type: Ta
|
||||
enable: true,
|
||||
endpoint: endpoint_url,
|
||||
auth_token: config.lookup(WEBHOOK_AUTH_TOKEN).unwrap_or_default(),
|
||||
queue_dir: queue_dir_or_default(config, WEBHOOK_QUEUE_DIR, default_queue_dir),
|
||||
queue_dir: config
|
||||
.lookup(WEBHOOK_QUEUE_DIR)
|
||||
.unwrap_or_else(|| default_queue_dir.to_string()),
|
||||
queue_limit: config
|
||||
.lookup(WEBHOOK_QUEUE_LIMIT)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
@@ -178,7 +180,9 @@ pub fn validate_webhook_config(config: &KVS, default_queue_dir: &str) -> Result<
|
||||
));
|
||||
}
|
||||
|
||||
let queue_dir = queue_dir_or_default(config, WEBHOOK_QUEUE_DIR, default_queue_dir);
|
||||
let queue_dir = config
|
||||
.lookup(WEBHOOK_QUEUE_DIR)
|
||||
.unwrap_or_else(|| default_queue_dir.to_string());
|
||||
if !queue_dir.is_empty() && !Path::new(&queue_dir).is_absolute() {
|
||||
return Err(TargetError::Configuration("Webhook queue directory must be an absolute path".to_string()));
|
||||
}
|
||||
@@ -230,7 +234,7 @@ pub fn build_mqtt_args(config: &KVS, default_queue_dir: &str, target_type: Targe
|
||||
config.lookup(MQTT_TLS_TRUST_LEAF_AS_CA).as_deref(),
|
||||
config.lookup(MQTT_WS_PATH_ALLOWLIST).as_deref(),
|
||||
)?,
|
||||
queue_dir: queue_dir_or_default(config, MQTT_QUEUE_DIR, default_queue_dir),
|
||||
queue_dir: config.lookup(MQTT_QUEUE_DIR).unwrap_or_else(|| default_queue_dir.to_string()),
|
||||
queue_limit: config
|
||||
.lookup(MQTT_QUEUE_LIMIT)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
@@ -307,7 +311,7 @@ pub fn build_nats_args(config: &KVS, default_queue_dir: &str, target_type: Targe
|
||||
tls_client_cert: config.lookup(NATS_TLS_CLIENT_CERT).unwrap_or_default(),
|
||||
tls_client_key: config.lookup(NATS_TLS_CLIENT_KEY).unwrap_or_default(),
|
||||
tls_required: parse_target_bool(config.lookup(NATS_TLS_REQUIRED).as_deref()).unwrap_or(false),
|
||||
queue_dir: queue_dir_or_default(config, NATS_QUEUE_DIR, default_queue_dir),
|
||||
queue_dir: config.lookup(NATS_QUEUE_DIR).unwrap_or_else(|| default_queue_dir.to_string()),
|
||||
queue_limit: config
|
||||
.lookup(NATS_QUEUE_LIMIT)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
@@ -349,7 +353,9 @@ pub fn build_pulsar_args(config: &KVS, default_queue_dir: &str, target_type: Tar
|
||||
tls_ca: config.lookup(PULSAR_TLS_CA).unwrap_or_default(),
|
||||
tls_allow_insecure: parse_target_bool(config.lookup(PULSAR_TLS_ALLOW_INSECURE).as_deref()).unwrap_or(false),
|
||||
tls_hostname_verification: parse_target_bool(config.lookup(PULSAR_TLS_HOSTNAME_VERIFICATION).as_deref()).unwrap_or(true),
|
||||
queue_dir: queue_dir_or_default(config, PULSAR_QUEUE_DIR, default_queue_dir),
|
||||
queue_dir: config
|
||||
.lookup(PULSAR_QUEUE_DIR)
|
||||
.unwrap_or_else(|| default_queue_dir.to_string()),
|
||||
queue_limit: config
|
||||
.lookup(PULSAR_QUEUE_LIMIT)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
@@ -399,7 +405,9 @@ pub fn build_redis_args(
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.map(Duration::from_secs)
|
||||
.unwrap_or_else(|| Duration::from_secs(15)),
|
||||
queue_dir: queue_dir_or_default(config, REDIS_QUEUE_DIR, default_queue_dir),
|
||||
queue_dir: config
|
||||
.lookup(REDIS_QUEUE_DIR)
|
||||
.unwrap_or_else(|| default_queue_dir.to_string()),
|
||||
queue_limit: config
|
||||
.lookup(REDIS_QUEUE_LIMIT)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
@@ -455,7 +463,9 @@ pub fn build_postgres_args(config: &KVS, default_queue_dir: &str, target_type: T
|
||||
tls_ca: config.lookup(POSTGRES_TLS_CA).unwrap_or_default(),
|
||||
tls_client_cert: config.lookup(POSTGRES_TLS_CLIENT_CERT).unwrap_or_default(),
|
||||
tls_client_key: config.lookup(POSTGRES_TLS_CLIENT_KEY).unwrap_or_default(),
|
||||
queue_dir: queue_dir_or_default(config, POSTGRES_QUEUE_DIR, default_queue_dir),
|
||||
queue_dir: config
|
||||
.lookup(POSTGRES_QUEUE_DIR)
|
||||
.unwrap_or_else(|| default_queue_dir.to_string()),
|
||||
queue_limit: config
|
||||
.lookup(POSTGRES_QUEUE_LIMIT)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
@@ -519,7 +529,9 @@ pub fn build_kafka_args(config: &KVS, default_queue_dir: &str, target_type: Targ
|
||||
},
|
||||
sasl_username,
|
||||
sasl_password,
|
||||
queue_dir: queue_dir_or_default(config, KAFKA_QUEUE_DIR, default_queue_dir),
|
||||
queue_dir: config
|
||||
.lookup(KAFKA_QUEUE_DIR)
|
||||
.unwrap_or_else(|| default_queue_dir.to_string()),
|
||||
queue_limit: config
|
||||
.lookup(KAFKA_QUEUE_LIMIT)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
@@ -557,7 +569,9 @@ pub fn build_mysql_args(config: &KVS, default_queue_dir: &str, target_type: Targ
|
||||
tls_ca: config.lookup(MYSQL_TLS_CA).unwrap_or_default(),
|
||||
tls_client_cert: config.lookup(MYSQL_TLS_CLIENT_CERT).unwrap_or_default(),
|
||||
tls_client_key: config.lookup(MYSQL_TLS_CLIENT_KEY).unwrap_or_default(),
|
||||
queue_dir: queue_dir_or_default(config, MYSQL_QUEUE_DIR, default_queue_dir),
|
||||
queue_dir: config
|
||||
.lookup(MYSQL_QUEUE_DIR)
|
||||
.unwrap_or_else(|| default_queue_dir.to_string()),
|
||||
queue_limit: config
|
||||
.lookup(MYSQL_QUEUE_LIMIT)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
|
||||
@@ -15,13 +15,12 @@
|
||||
use crate::error::StoreError;
|
||||
use rustfs_config::notify::{COMPRESS_EXT, DEFAULT_EXT};
|
||||
use rustfs_config::{
|
||||
AUDIT_DEFAULT_DIR, DEFAULT_LIMIT, DEFAULT_TARGET_STORE_COMPRESS, DEFAULT_TARGET_STORE_MAX_BYTES, ENV_TARGET_STORE_COMPRESS,
|
||||
ENV_TARGET_STORE_DIR, ENV_TARGET_STORE_MAX_BYTES, EVENT_DEFAULT_DIR, EnableState,
|
||||
DEFAULT_LIMIT, DEFAULT_TARGET_STORE_COMPRESS, DEFAULT_TARGET_STORE_MAX_BYTES, ENV_TARGET_STORE_COMPRESS,
|
||||
ENV_TARGET_STORE_MAX_BYTES, EnableState,
|
||||
};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use snap::raw::{Decoder, Encoder};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::Write,
|
||||
@@ -156,46 +155,6 @@ fn queue_store_compression_enabled() -> bool {
|
||||
resolve_queue_store_compression_from_env_value(value.as_deref())
|
||||
}
|
||||
|
||||
/// Map a configured target `queue_dir` to the directory this node opens.
|
||||
///
|
||||
/// `EVENT_DEFAULT_DIR` and `AUDIT_DEFAULT_DIR` are persisted cluster-wide as logical
|
||||
/// defaults, so every node resolves them locally. `RUSTFS_TARGET_STORE_DIR` relocates
|
||||
/// their root on any platform. Without it, Windows uses `%ProgramData%\RustFS`, because
|
||||
/// the Unix literal has no drive prefix and is not an absolute path there. Every other
|
||||
/// value, including an explicit operator path, is returned unchanged.
|
||||
pub fn resolve_queue_dir(queue_dir: &str) -> Cow<'_, str> {
|
||||
let store_root = std::env::var(ENV_TARGET_STORE_DIR).ok();
|
||||
resolve_queue_dir_with(queue_dir, store_root.as_deref(), platform_queue_root().as_deref())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn platform_queue_root() -> Option<PathBuf> {
|
||||
std::env::var_os("ProgramData")
|
||||
.filter(|root| !root.is_empty())
|
||||
.map(|root| PathBuf::from(root).join("RustFS"))
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn platform_queue_root() -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
fn resolve_queue_dir_with<'a>(queue_dir: &'a str, store_root: Option<&str>, platform_root: Option<&Path>) -> Cow<'a, str> {
|
||||
let leaf = match queue_dir {
|
||||
EVENT_DEFAULT_DIR => "events",
|
||||
AUDIT_DEFAULT_DIR => "audit",
|
||||
_ => return Cow::Borrowed(queue_dir),
|
||||
};
|
||||
let root = match store_root.map(str::trim).filter(|root| !root.is_empty()) {
|
||||
Some(root) => Path::new(root),
|
||||
None => match platform_root {
|
||||
Some(root) => root,
|
||||
None => return Cow::Borrowed(queue_dir),
|
||||
},
|
||||
};
|
||||
Cow::Owned(root.join(leaf).to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
/// Represents a key for an entry in the store
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Key {
|
||||
@@ -1406,56 +1365,6 @@ mod tests {
|
||||
thread,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn logical_default_queue_dirs_resolve_under_the_configured_root() {
|
||||
let root = Path::new("/srv/rustfs-queues");
|
||||
let platform = Path::new("/platform/RustFS");
|
||||
assert_eq!(
|
||||
resolve_queue_dir_with(EVENT_DEFAULT_DIR, Some("/srv/rustfs-queues"), Some(platform)),
|
||||
root.join("events").to_string_lossy()
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_queue_dir_with(AUDIT_DEFAULT_DIR, Some(" /srv/rustfs-queues "), None),
|
||||
root.join("audit").to_string_lossy()
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_queue_dir_with(EVENT_DEFAULT_DIR, Some(" "), Some(platform)),
|
||||
platform.join("events").to_string_lossy(),
|
||||
"a blank override must fall back to the platform root"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_and_unmapped_queue_dirs_are_preserved() {
|
||||
let platform = Path::new("/platform/RustFS");
|
||||
for explicit in [
|
||||
"/var/lib/rustfs/events",
|
||||
"/opt/rustfs/events/",
|
||||
"/opt/rustfs",
|
||||
"",
|
||||
"relative/queue",
|
||||
] {
|
||||
assert_eq!(
|
||||
resolve_queue_dir_with(explicit, Some("/srv/rustfs-queues"), Some(platform)),
|
||||
explicit,
|
||||
"only the exact logical defaults may be relocated"
|
||||
);
|
||||
}
|
||||
assert_eq!(resolve_queue_dir_with(EVENT_DEFAULT_DIR, None, None), EVENT_DEFAULT_DIR);
|
||||
assert_eq!(resolve_queue_dir_with(AUDIT_DEFAULT_DIR, None, None), AUDIT_DEFAULT_DIR);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn windows_logical_default_queue_dirs_are_absolute_without_an_override() {
|
||||
let platform = platform_queue_root().expect("Windows defines ProgramData");
|
||||
for default in [EVENT_DEFAULT_DIR, AUDIT_DEFAULT_DIR] {
|
||||
let resolved = resolve_queue_dir_with(default, None, Some(&platform));
|
||||
assert!(Path::new(resolved.as_ref()).is_absolute(), "{default} resolved to {resolved}");
|
||||
assert!(resolved.contains("RustFS"), "{default} resolved to {resolved}");
|
||||
}
|
||||
}
|
||||
|
||||
fn temp_store_dir(name: &str) -> PathBuf {
|
||||
std::env::temp_dir().join(format!("rustfs-targets-{name}-{}", Uuid::new_v4()))
|
||||
}
|
||||
|
||||
@@ -1,22 +1,4 @@
|
||||
# Target queue storage
|
||||
|
||||
## Queue directory location
|
||||
|
||||
A target's `queue_dir` is stored in the cluster-wide target configuration. The built-in defaults `/opt/rustfs/events` and `/opt/rustfs/audit` are logical defaults: each node resolves them to a local directory when it validates or opens a queue. The stock notify and audit default tables both persist `/opt/rustfs/events`; `/opt/rustfs/audit` applies only to an audit target whose configuration has no `queue_dir` key. Any other `queue_dir`, including an explicit operator path, is used as written and must be absolute.
|
||||
|
||||
| Node setting | `/opt/rustfs/events` resolves to | `/opt/rustfs/audit` resolves to |
|
||||
|---|---|---|
|
||||
| `RUSTFS_TARGET_STORE_DIR=<root>` (any platform) | `<root>/events` | `<root>/audit` |
|
||||
| Windows, variable unset | `%ProgramData%\RustFS\events` | `%ProgramData%\RustFS\audit` |
|
||||
| Linux/macOS, variable unset | `/opt/rustfs/events` | `/opt/rustfs/audit` |
|
||||
|
||||
`RUSTFS_TARGET_STORE_DIR` must be an absolute path; a relative value makes target validation fail with the queue-directory error. The queue store creates missing directories when it opens, so the resolved directory only needs to be creatable by the RustFS process. An admin request that names a default path explicitly is checked at its resolved location and, as for any explicit `queue_dir`, that directory must already exist.
|
||||
|
||||
The official container image links `/opt/rustfs/events` and `/opt/rustfs/audit` into `/data/.rustfs`, so its queues survive container recreation without this variable. Windows previously rejected targets that kept the default because the Unix literal is not an absolute Windows path; those targets now validate and queue under `%ProgramData%\RustFS`.
|
||||
|
||||
Setting or changing `RUSTFS_TARGET_STORE_DIR` on an existing node moves where its default queues are opened. Entries still pending in the previous directory are not migrated: let them drain first, or move the directory contents while RustFS is stopped. Rolling back to a version without this variable returns default queues to the previous location.
|
||||
|
||||
## Queue size limits
|
||||
# Target queue storage limits
|
||||
|
||||
Disk queues for notification and audit targets enforce both their configured entry limit and a shared byte limit for each queue directory. `RUSTFS_TARGET_STORE_MAX_BYTES` sets that byte limit; the default is 1 GiB. The value must be a positive integer in bytes. Missing, zero, invalid, and overflowing values use the default.
|
||||
|
||||
|
||||
@@ -157,7 +157,6 @@ behavior.
|
||||
| config.rustfs.obs_environment | string | `"development"` | |
|
||||
| config.rustfs.obs_log_directory | string | `"/logs"` | Log directory inside the RustFS container. Set to `""` to disable log PVCs and mounts. |
|
||||
| config.rustfs.region | string | `"us-east-1"` | |
|
||||
| config.rustfs.target_store_dir | string | `""` | Absolute root for notify and audit target queues that keep the default `queue_dir`, set as `RUSTFS_TARGET_STORE_DIR`; queues move to `<root>/events` and `<root>/audit`. Empty keeps the image layout, where `/opt/rustfs/events` and `/opt/rustfs/audit` link into `/data/.rustfs`. |
|
||||
| config.rustfs.volumes | string | `""` | Explicit distributed volume topology. When empty, the chart generates the topology and normally injects `RUSTFS_LOCAL_ENDPOINT_HOST`; custom topologies must configure local endpoint identity explicitly when needed. |
|
||||
| config.rustfs.log_rotation.size | int | `"100"` | Default log rotation size mb for rustfs. |
|
||||
| config.rustfs.log_rotation.time | string | `"hour"` | Default log rotation time for rustfs. |
|
||||
|
||||
@@ -21,9 +21,6 @@ data:
|
||||
{{- if .region }}
|
||||
RUSTFS_REGION: {{ .region | quote }}
|
||||
{{- end }}
|
||||
{{- if .target_store_dir }}
|
||||
RUSTFS_TARGET_STORE_DIR: {{ .target_store_dir | quote }}
|
||||
{{- end }}
|
||||
{{- if .domains }}
|
||||
RUSTFS_SERVER_DOMAINS: {{ include "rustfs.serverDomains" $ | quote }}
|
||||
{{- end }}
|
||||
|
||||
@@ -143,9 +143,6 @@ config:
|
||||
console_enable: "true"
|
||||
console_address: ":9001"
|
||||
region: "us-east-1"
|
||||
# Root for notify/audit target queues that keep the default queue_dir; queues use
|
||||
# <root>/events and <root>/audit. Empty keeps the image layout (/opt/rustfs -> /data/.rustfs).
|
||||
target_store_dir: ""
|
||||
# Optionally enable support for virtual-hosted-style requests.
|
||||
# See more information: https://docs.rustfs.com/integration/virtual.html
|
||||
domains: "" # e.g. "example.com"
|
||||
|
||||
@@ -560,8 +560,6 @@ where
|
||||
}
|
||||
|
||||
pub(crate) async fn validate_queue_dir(queue_dir: &str) -> S3Result<()> {
|
||||
let queue_dir = rustfs_targets::store::resolve_queue_dir(queue_dir);
|
||||
let queue_dir = queue_dir.as_ref();
|
||||
if !queue_dir.is_empty() {
|
||||
if !Path::new(queue_dir).is_absolute() {
|
||||
return Err(s3_error!(InvalidArgument, "queue_dir must be an absolute path"));
|
||||
@@ -1076,58 +1074,3 @@ mod instance_runtime_status_tests {
|
||||
assert!(statuses.contains_key(&("PRIMARY".into(), "custom-two".into())));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod queue_dir_resolution_tests {
|
||||
use super::validate_queue_dir;
|
||||
use rustfs_config::server_config::KVS;
|
||||
use rustfs_config::{AUDIT_DEFAULT_DIR, ENV_TARGET_STORE_DIR, EVENT_DEFAULT_DIR, REDIS_QUEUE_DIR, REDIS_URL};
|
||||
use rustfs_targets::{config::build_redis_args, target::TargetType};
|
||||
use serial_test::serial;
|
||||
|
||||
fn redis_config() -> KVS {
|
||||
let mut config = KVS::new();
|
||||
config.insert(REDIS_URL.to_string(), "redis://127.0.0.1:6379/".to_string());
|
||||
config
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn default_queue_dirs_follow_the_target_store_root() {
|
||||
let root = tempfile::tempdir().expect("queue store root");
|
||||
let root_text = root.path().to_str().expect("UTF-8 queue store root").to_owned();
|
||||
temp_env::async_with_vars([(ENV_TARGET_STORE_DIR, Some(root_text.as_str()))], async {
|
||||
let notify = build_redis_args(&redis_config(), EVENT_DEFAULT_DIR, "events", TargetType::NotifyEvent)
|
||||
.expect("default notify queue");
|
||||
assert_eq!(notify.queue_dir, root.path().join("events").to_string_lossy());
|
||||
let audit =
|
||||
build_redis_args(&redis_config(), AUDIT_DEFAULT_DIR, "audit", TargetType::AuditLog).expect("default audit queue");
|
||||
assert_eq!(audit.queue_dir, root.path().join("audit").to_string_lossy());
|
||||
|
||||
let explicit = root.path().join("operator-queue").to_string_lossy().into_owned();
|
||||
let mut config = redis_config();
|
||||
config.insert(REDIS_QUEUE_DIR.to_string(), explicit.clone());
|
||||
let args = build_redis_args(&config, EVENT_DEFAULT_DIR, "events", TargetType::NotifyEvent).expect("explicit queue");
|
||||
assert_eq!(args.queue_dir, explicit, "an operator path must not be relocated");
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn explicit_logical_default_is_validated_at_its_resolved_directory() {
|
||||
let root = tempfile::tempdir().expect("queue store root");
|
||||
let root_text = root.path().to_str().expect("UTF-8 queue store root").to_owned();
|
||||
temp_env::async_with_vars([(ENV_TARGET_STORE_DIR, Some(root_text.as_str()))], async {
|
||||
let missing = validate_queue_dir(EVENT_DEFAULT_DIR)
|
||||
.await
|
||||
.expect_err("the resolved queue directory does not exist yet");
|
||||
assert!(missing.message().is_some_and(|message| message.contains("does not exist")));
|
||||
std::fs::create_dir(root.path().join("events")).expect("resolved queue directory");
|
||||
validate_queue_dir(EVENT_DEFAULT_DIR)
|
||||
.await
|
||||
.expect("the logical default must be checked where this node stores it");
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise the release download entry point without fetching external tools."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[1] / ".github/actions/setup/download_release_asset.py"
|
||||
|
||||
|
||||
class ReleaseAssetTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.body = b"verified tool fixture"
|
||||
self.sha256 = hashlib.sha256(self.body).hexdigest()
|
||||
self.asset = {"name": "tool.zip", "id": 17, "size": len(self.body), "digest": "sha256:" + self.sha256}
|
||||
self.metadata = {"tag_name": "v1.0", "assets": [self.asset]}
|
||||
self.output = self.root / "tool.zip"
|
||||
self.output.write_bytes(b"previous file")
|
||||
self.calls = self.root / "calls.jsonl"
|
||||
gh = self.root / "gh"
|
||||
gh.write_text(f"#!{sys.executable}\n" + '''import json,os,pathlib,sys
|
||||
args=sys.argv[1:]
|
||||
with pathlib.Path(os.environ['CALLS']).open('a') as out:out.write(json.dumps(args)+'\\n')
|
||||
if '/releases/tags/' in args[-1]:
|
||||
if os.environ.get('FAIL_METADATA'):raise SystemExit(19)
|
||||
print(os.environ['METADATA'])
|
||||
else:
|
||||
if os.environ.get('FAIL_DOWNLOAD'):raise SystemExit(23)
|
||||
sys.stdout.buffer.write(bytes.fromhex(os.environ['BODY']))
|
||||
''')
|
||||
gh.chmod(0o755)
|
||||
self.env = dict(os.environ, PATH=str(self.root) + os.pathsep + os.environ["PATH"], CALLS=str(self.calls), GH_TOKEN="synthetic-do-not-log-token")
|
||||
|
||||
def invoke(self, extra=(), **env):
|
||||
environment = dict(self.env, METADATA=json.dumps(self.metadata), BODY=self.body.hex(), **env)
|
||||
return subprocess.run([sys.executable, str(SCRIPT), "--repository", "owner/repo", "--tag", "v1.0", "--asset", "tool.zip", "--output", str(self.output), *extra], env=environment, capture_output=True, text=True)
|
||||
|
||||
def assert_failed_atomically(self, result):
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual(self.output.read_bytes(), b"previous file")
|
||||
self.assertEqual(list(self.root.glob(".release-asset-*")), [])
|
||||
self.assertNotIn(self.env["GH_TOKEN"], result.stdout + result.stderr)
|
||||
|
||||
def test_valid_asset_uses_api_and_installs_only_verified_bytes(self):
|
||||
result = self.invoke(("--sha256", self.sha256))
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(self.output.read_bytes(), self.body)
|
||||
calls = [json.loads(line) for line in self.calls.read_text().splitlines()]
|
||||
self.assertEqual(calls[0], ["api", "--hostname", "github.com", "repos/owner/repo/releases/tags/v1.0"])
|
||||
self.assertEqual(calls[1], ["api", "--hostname", "github.com", "--header", "Accept: application/octet-stream", "repos/owner/repo/releases/assets/17"])
|
||||
self.assertNotIn(self.env["GH_TOKEN"], json.dumps(calls) + result.stdout + result.stderr)
|
||||
|
||||
def test_download_failure_preserves_destination(self):
|
||||
self.assert_failed_atomically(self.invoke(FAIL_DOWNLOAD="1"))
|
||||
|
||||
def test_missing_gh_fails_without_replacing_destination(self):
|
||||
(self.root / "gh").unlink()
|
||||
self.env["PATH"] = str(self.root)
|
||||
self.assert_failed_atomically(self.invoke())
|
||||
|
||||
def test_metadata_failure_preserves_destination(self):
|
||||
self.assert_failed_atomically(self.invoke(FAIL_METADATA="1"))
|
||||
|
||||
def test_corrupt_download_fails_digest_validation(self):
|
||||
self.body = b"modified tool fixture"
|
||||
self.assert_failed_atomically(self.invoke())
|
||||
|
||||
def test_truncated_download_fails_size_validation(self):
|
||||
self.asset["size"] += 1
|
||||
self.assert_failed_atomically(self.invoke())
|
||||
|
||||
def test_pinned_digest_cannot_be_replaced_by_release_metadata(self):
|
||||
self.assert_failed_atomically(self.invoke(("--sha256", "0" * 64)))
|
||||
self.assertEqual(len(self.calls.read_text().splitlines()), 1)
|
||||
|
||||
def test_ambiguous_or_missing_assets_fail_before_download(self):
|
||||
for assets in ([], [self.asset, self.asset]):
|
||||
with self.subTest(assets=len(assets)):
|
||||
self.metadata["assets"] = assets
|
||||
self.assert_failed_atomically(self.invoke())
|
||||
|
||||
def test_metadata_tag_must_match_requested_version(self):
|
||||
self.metadata["tag_name"] = "v2.0"
|
||||
self.assert_failed_atomically(self.invoke())
|
||||
|
||||
def test_malformed_release_inventory_fails_before_download(self):
|
||||
for metadata in ([], {"tag_name": "v1.0", "assets": None}, {"tag_name": "v1.0", "assets": [None]}):
|
||||
with self.subTest(metadata=metadata):
|
||||
self.metadata = metadata
|
||||
self.assert_failed_atomically(self.invoke())
|
||||
|
||||
def test_missing_digest_or_invalid_identity_fails_before_download(self):
|
||||
for field, value in [("digest", None), ("digest", "sha256:invalid"), ("id", True), ("id", 0), ("size", 0), ("size", 513 * 1024 * 1024)]:
|
||||
with self.subTest(field=field, value=value):
|
||||
self.asset[field] = value
|
||||
self.assert_failed_atomically(self.invoke())
|
||||
self.asset = {"name": "tool.zip", "id": 17, "size": len(self.body), "digest": "sha256:" + self.sha256}
|
||||
self.metadata["assets"] = [self.asset]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -206,13 +206,6 @@ grep -q 'RUSTFS_SCANNER_ALERT_EXCESS_VERSIONS: "100"' <<<"$scanner_config_output
|
||||
grep -q 'RUSTFS_SCANNER_ALERT_EXCESS_VERSION_SIZE: "1099511627776"' <<<"$scanner_config_output"
|
||||
grep -q 'RUSTFS_SCANNER_ALERT_EXCESS_FOLDERS: "65538"' <<<"$scanner_config_output"
|
||||
|
||||
target_store_output=$(render_chart --set config.rustfs.target_store_dir=/data/rustfs0/.rustfs)
|
||||
grep -q 'RUSTFS_TARGET_STORE_DIR: "/data/rustfs0/.rustfs"' <<<"$target_store_output"
|
||||
if grep -q 'RUSTFS_TARGET_STORE_DIR' <<<"$(render_chart)"; then
|
||||
echo "RUSTFS_TARGET_STORE_DIR must not be rendered by default" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fail-closed credential checks. Rendering must fail when no credentials,
|
||||
# existingSecret, or allowInsecureDefaults override is supplied.
|
||||
default_render_status=0
|
||||
|
||||
Reference in New Issue
Block a user