mirror of
https://github.com/rustfs/rustfs.git
synced 2026-11-08 19:43:05 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b1cdaed49e |
@@ -7,6 +7,10 @@ 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.
|
||||
- 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).
|
||||
- `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,6 +276,13 @@ 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,6 +144,12 @@ 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_config::EVENT_DEFAULT_DIR,
|
||||
rustfs_targets::store::resolve_queue_dir(rustfs_config::EVENT_DEFAULT_DIR),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
// 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;
|
||||
@@ -29,6 +30,12 @@ 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) {
|
||||
@@ -124,7 +131,7 @@ pub(super) fn validate_nats_server_config(server: &ServerAddr, config: &KVS, def
|
||||
));
|
||||
}
|
||||
|
||||
let queue_dir = config.lookup(NATS_QUEUE_DIR).unwrap_or_else(|| default_queue_dir.to_string());
|
||||
let queue_dir = queue_dir_or_default(config, NATS_QUEUE_DIR, default_queue_dir);
|
||||
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()));
|
||||
}
|
||||
@@ -204,9 +211,7 @@ pub(super) fn validate_pulsar_broker_config(broker: &str, config: &KVS, default_
|
||||
));
|
||||
}
|
||||
|
||||
let queue_dir = config
|
||||
.lookup(PULSAR_QUEUE_DIR)
|
||||
.unwrap_or_else(|| default_queue_dir.to_string());
|
||||
let queue_dir = queue_dir_or_default(config, PULSAR_QUEUE_DIR, default_queue_dir);
|
||||
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, 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, queue_dir_or_default,
|
||||
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: config.lookup(AMQP_QUEUE_DIR).unwrap_or_else(|| default_queue_dir.to_string()),
|
||||
queue_dir: queue_dir_or_default(config, AMQP_QUEUE_DIR, default_queue_dir),
|
||||
queue_limit: config
|
||||
.lookup(AMQP_QUEUE_LIMIT)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
@@ -146,9 +146,7 @@ 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: config
|
||||
.lookup(WEBHOOK_QUEUE_DIR)
|
||||
.unwrap_or_else(|| default_queue_dir.to_string()),
|
||||
queue_dir: queue_dir_or_default(config, WEBHOOK_QUEUE_DIR, default_queue_dir),
|
||||
queue_limit: config
|
||||
.lookup(WEBHOOK_QUEUE_LIMIT)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
@@ -180,9 +178,7 @@ pub fn validate_webhook_config(config: &KVS, default_queue_dir: &str) -> Result<
|
||||
));
|
||||
}
|
||||
|
||||
let queue_dir = config
|
||||
.lookup(WEBHOOK_QUEUE_DIR)
|
||||
.unwrap_or_else(|| default_queue_dir.to_string());
|
||||
let queue_dir = queue_dir_or_default(config, WEBHOOK_QUEUE_DIR, default_queue_dir);
|
||||
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()));
|
||||
}
|
||||
@@ -234,7 +230,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: config.lookup(MQTT_QUEUE_DIR).unwrap_or_else(|| default_queue_dir.to_string()),
|
||||
queue_dir: queue_dir_or_default(config, MQTT_QUEUE_DIR, default_queue_dir),
|
||||
queue_limit: config
|
||||
.lookup(MQTT_QUEUE_LIMIT)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
@@ -311,7 +307,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: config.lookup(NATS_QUEUE_DIR).unwrap_or_else(|| default_queue_dir.to_string()),
|
||||
queue_dir: queue_dir_or_default(config, NATS_QUEUE_DIR, default_queue_dir),
|
||||
queue_limit: config
|
||||
.lookup(NATS_QUEUE_LIMIT)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
@@ -353,9 +349,7 @@ 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: config
|
||||
.lookup(PULSAR_QUEUE_DIR)
|
||||
.unwrap_or_else(|| default_queue_dir.to_string()),
|
||||
queue_dir: queue_dir_or_default(config, PULSAR_QUEUE_DIR, default_queue_dir),
|
||||
queue_limit: config
|
||||
.lookup(PULSAR_QUEUE_LIMIT)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
@@ -405,9 +399,7 @@ 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: config
|
||||
.lookup(REDIS_QUEUE_DIR)
|
||||
.unwrap_or_else(|| default_queue_dir.to_string()),
|
||||
queue_dir: queue_dir_or_default(config, REDIS_QUEUE_DIR, default_queue_dir),
|
||||
queue_limit: config
|
||||
.lookup(REDIS_QUEUE_LIMIT)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
@@ -463,9 +455,7 @@ 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: config
|
||||
.lookup(POSTGRES_QUEUE_DIR)
|
||||
.unwrap_or_else(|| default_queue_dir.to_string()),
|
||||
queue_dir: queue_dir_or_default(config, POSTGRES_QUEUE_DIR, default_queue_dir),
|
||||
queue_limit: config
|
||||
.lookup(POSTGRES_QUEUE_LIMIT)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
@@ -529,9 +519,7 @@ pub fn build_kafka_args(config: &KVS, default_queue_dir: &str, target_type: Targ
|
||||
},
|
||||
sasl_username,
|
||||
sasl_password,
|
||||
queue_dir: config
|
||||
.lookup(KAFKA_QUEUE_DIR)
|
||||
.unwrap_or_else(|| default_queue_dir.to_string()),
|
||||
queue_dir: queue_dir_or_default(config, KAFKA_QUEUE_DIR, default_queue_dir),
|
||||
queue_limit: config
|
||||
.lookup(KAFKA_QUEUE_LIMIT)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
@@ -569,9 +557,7 @@ 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: config
|
||||
.lookup(MYSQL_QUEUE_DIR)
|
||||
.unwrap_or_else(|| default_queue_dir.to_string()),
|
||||
queue_dir: queue_dir_or_default(config, MYSQL_QUEUE_DIR, default_queue_dir),
|
||||
queue_limit: config
|
||||
.lookup(MYSQL_QUEUE_LIMIT)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
|
||||
@@ -15,12 +15,13 @@
|
||||
use crate::error::StoreError;
|
||||
use rustfs_config::notify::{COMPRESS_EXT, DEFAULT_EXT};
|
||||
use rustfs_config::{
|
||||
DEFAULT_LIMIT, DEFAULT_TARGET_STORE_COMPRESS, DEFAULT_TARGET_STORE_MAX_BYTES, ENV_TARGET_STORE_COMPRESS,
|
||||
ENV_TARGET_STORE_MAX_BYTES, EnableState,
|
||||
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,
|
||||
};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use snap::raw::{Decoder, Encoder};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::Write,
|
||||
@@ -155,6 +156,46 @@ 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 {
|
||||
@@ -1365,6 +1406,56 @@ 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,4 +1,22 @@
|
||||
# Target queue storage limits
|
||||
# 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
|
||||
|
||||
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,6 +157,7 @@ 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,6 +21,9 @@ 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,6 +143,9 @@ 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,6 +560,8 @@ 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"));
|
||||
@@ -1074,3 +1076,58 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +206,13 @@ 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