Compare commits

...

2 Commits

Author SHA1 Message Date
Copilot
29c004d935 feat: enhance console separation with enterprise-grade security, monitoring, and advanced tower-http integration (#513)
* Initial plan

* feat: implement console service separation from endpoint

Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>

* feat: add console separation documentation and tests

Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>

* feat: enhance console separation with configurable CORS and improved Docker support

Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>

* feat: implement enhanced console separation with security hardening and monitoring

Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>

* refactor: implement console TLS following endpoint logic and improve configuration

Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>

* add tower-http feature "timeout|limit"

* add dependencies crates `axum-server`

* refactor: reconstruct console server with enhanced tower-http features and environment variables

Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>

* upgrade dep

* improve code for dns and console port `:9001`

* improve code

* fix

* docs: comprehensive improvement of console separation documentation and Docker deployment standards

Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>

* fmt

* add logs

* improve code for Config handler

* remove logs

* fix

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
2025-09-13 14:48:14 +08:00
majinghe
4595bf7db6 fix docker compose running with no such file error (#519)
* fix docker compose running with no such file error

* fix observability docker compose
2025-09-13 13:04:06 +08:00
40 changed files with 4346 additions and 710 deletions

View File

@@ -14,18 +14,27 @@
services:
tempo-init:
image: busybox:latest
command: ["sh", "-c", "chown -R 10001:10001 /var/tempo"]
volumes:
- ./tempo-data:/var/tempo
user: root
networks:
- otel-network
restart: "no"
tempo:
image: grafana/tempo:latest
#user: root # The container must be started with root to execute chown in the script
#entrypoint: [ "/etc/tempo/entrypoint.sh" ] # Specify a custom entry point
user: "10001" # The container must be started with root to execute chown in the script
command: [ "-config.file=/etc/tempo.yaml" ] # This is passed as a parameter to the entry point script
volumes:
- ./tempo-entrypoint.sh:/etc/tempo/entrypoint.sh # Mount entry point script
- ./tempo.yaml:/etc/tempo.yaml
- ./tempo.yaml:/etc/tempo.yaml:ro
- ./tempo-data:/var/tempo
ports:
- "3200:3200" # tempo
- "24317:4317" # otlp grpc
restart: unless-stopped
networks:
- otel-network
@@ -94,4 +103,4 @@ networks:
driver: bridge
name: "network_otel_config"
driver_opts:
com.docker.network.enable_ipv6: "true"
com.docker.network.enable_ipv6: "true"

View File

@@ -1,8 +0,0 @@
#!/bin/sh
# Run as root to fix directory permissions
chown -R 10001:10001 /var/tempo
# Use su-exec (a lightweight sudo/gosu alternative, commonly used in Alpine mirroring)
# Switch to user 10001 and execute the original command (CMD) passed to the script
# "$@" represents all parameters passed to this script, i.e. command in docker-compose
exec su-exec 10001:10001 /tempo "$@"

4
.gitignore vendored
View File

@@ -20,4 +20,6 @@ profile.json
.docker/openobserve-otel/data
*.zst
.secrets
*.go
*.go
*.pb
*.svg

574
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -100,6 +100,8 @@ atomic_enum = "0.3.0"
aws-config = { version = "1.8.6" }
aws-sdk-s3 = "1.101.0"
axum = "0.8.4"
axum-extra = "0.10.1"
axum-server = "0.7.2"
base64-simd = "0.8.0"
base64 = "0.22.1"
brotli = "8.0.2"
@@ -109,11 +111,12 @@ byteorder = "1.5.0"
cfg-if = "1.0.3"
crc-fast = "1.5.0"
chacha20poly1305 = { version = "0.10.1" }
chrono = { version = "0.4.41", features = ["serde"] }
clap = { version = "4.5.46", features = ["derive", "env"] }
const-str = { version = "0.6.4", features = ["std", "proc"] }
chrono = { version = "0.4.42", features = ["serde"] }
clap = { version = "4.5.47", features = ["derive", "env"] }
const-str = { version = "0.7.0", features = ["std", "proc"] }
crc32fast = "1.5.0"
criterion = { version = "0.7", features = ["html_reports"] }
crossbeam-queue = "0.3.12"
dashmap = "6.1.0"
datafusion = "46.0.1"
derive_builder = "0.20.2"
@@ -126,6 +129,7 @@ futures = "0.3.31"
futures-core = "0.3.31"
futures-util = "0.3.31"
glob = "0.3.3"
heapless = "0.9.1"
hex = "0.4.3"
hex-simd = "0.8.0"
highway = { version = "1.3.0" }
@@ -141,7 +145,7 @@ hyper-util = { version = "0.1.16", features = [
hyper-rustls = "0.27.7"
http = "1.3.1"
http-body = "1.0.1"
humantime = "2.2.0"
humantime = "2.3.0"
ipnetwork = { version = "0.21.1", features = ["serde"] }
jsonwebtoken = "9.3.1"
lazy_static = "1.5.0"
@@ -196,11 +200,11 @@ reqwest = { version = "0.12.23", default-features = false, features = [
"json",
"blocking",
] }
rmcp = { version = "0.6.1" }
rmcp = { version = "0.6.4" }
rmp = "0.8.14"
rmp-serde = "1.3.0"
rsa = "0.9.8"
rumqttc = { version = "0.24" }
rumqttc = { version = "0.25.0" }
rust-embed = { version = "8.7.2" }
rustfs-rsc = "2025.506.1"
rustls = { version = "0.23.31" }
@@ -217,17 +221,18 @@ sha2 = "0.10.9"
shadow-rs = { version = "1.3.0", default-features = false }
siphasher = "1.0.1"
smallvec = { version = "1.15.1", features = ["serde"] }
snafu = "0.8.8"
smartstring = "1.0.1"
snafu = "0.8.9"
snap = "1.1.1"
socket2 = "0.6.0"
strum = { version = "0.27.2", features = ["derive"] }
sysinfo = "0.37.0"
sysctl = "0.6.0"
tempfile = "3.21.0"
tempfile = "3.22.0"
temp-env = "0.3.6"
test-case = "3.3.1"
thiserror = "2.0.16"
time = { version = "0.3.42", features = [
time = { version = "0.3.43", features = [
"std",
"parsing",
"formatting",
@@ -240,9 +245,9 @@ tokio-stream = { version = "0.1.17" }
tokio-tar = "0.3.1"
tokio-test = "0.4.4"
tokio-util = { version = "0.7.16", features = ["io", "compat"] }
tonic = { version = "0.14.1", features = ["gzip"] }
tonic-prost = { version = "0.14.1" }
tonic-prost-build = { version = "0.14.1" }
tonic = { version = "0.14.2", features = ["gzip"] }
tonic-prost = { version = "0.14.2" }
tonic-prost-build = { version = "0.14.2" }
tower = { version = "0.5.2", features = ["timeout"] }
tower-http = { version = "0.6.6", features = ["cors"] }
tracing = "0.1.41"
@@ -253,7 +258,7 @@ tracing-subscriber = { version = "0.3.20", features = ["env-filter", "time"] }
transform-stream = "0.3.1"
url = "2.5.7"
urlencoding = "2.1.3"
uuid = { version = "1.18.0", features = [
uuid = { version = "1.18.1", features = [
"v4",
"fast-rng",
"macro-diagnostics",

View File

@@ -69,15 +69,19 @@ RUN chmod +x /usr/bin/rustfs /entrypoint.sh && \
chmod 0750 /data /logs
ENV RUSTFS_ADDRESS=":9000" \
RUSTFS_CONSOLE_ADDRESS=":9001" \
RUSTFS_ACCESS_KEY="rustfsadmin" \
RUSTFS_SECRET_KEY="rustfsadmin" \
RUSTFS_CONSOLE_ENABLE="true" \
RUSTFS_EXTERNAL_ADDRESS="" \
RUSTFS_CORS_ALLOWED_ORIGINS="*" \
RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="*" \
RUSTFS_VOLUMES="/data" \
RUST_LOG="warn" \
RUSTFS_OBS_LOG_DIRECTORY="/logs" \
RUSTFS_SINKS_FILE_PATH="/logs"
EXPOSE 9000
EXPOSE 9000 9001
VOLUME ["/data", "/logs"]
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -45,4 +45,4 @@ tracing-subscriber = { workspace = true }
walkdir = "2.5.0"
tempfile = { workspace = true }
criterion = { workspace = true, features = ["html_reports"] }
sysinfo = "0.30.8"
sysinfo = { workspace = true }

View File

@@ -124,7 +124,7 @@ pub const DEFAULT_LOG_FILENAME: &str = "rustfs";
/// This is the default log filename for OBS.
/// It is used to store the logs of the application.
/// Default value: rustfs.log
pub const DEFAULT_OBS_LOG_FILENAME: &str = concat!(DEFAULT_LOG_FILENAME, ".");
pub const DEFAULT_OBS_LOG_FILENAME: &str = concat!(DEFAULT_LOG_FILENAME, "");
/// Default sink file log file for rustfs
/// This is the default sink file log file for rustfs.
@@ -160,6 +160,16 @@ pub const DEFAULT_LOG_ROTATION_TIME: &str = "day";
/// Environment variable: RUSTFS_OBS_LOG_KEEP_FILES
pub const DEFAULT_LOG_KEEP_FILES: u16 = 30;
/// This is the external address for rustfs to access endpoint (used in Docker deployments).
/// This should match the mapped host port when using Docker port mapping.
/// Example: ":9020" when mapping host port 9020 to container port 9000.
/// Default value: DEFAULT_ADDRESS
/// Environment variable: RUSTFS_EXTERNAL_ADDRESS
/// Command line argument: --external-address
/// Example: RUSTFS_EXTERNAL_ADDRESS=":9020"
/// Example: --external-address ":9020"
pub const ENV_EXTERNAL_ADDRESS: &str = "RUSTFS_EXTERNAL_ADDRESS";
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -0,0 +1,81 @@
// 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.
/// CORS allowed origins for the endpoint service
/// Comma-separated list of origins or "*" for all origins
pub const ENV_CORS_ALLOWED_ORIGINS: &str = "RUSTFS_CORS_ALLOWED_ORIGINS";
/// Default CORS allowed origins for the endpoint service
/// Comes from the console service default
/// See DEFAULT_CONSOLE_CORS_ALLOWED_ORIGINS
pub const DEFAULT_CORS_ALLOWED_ORIGINS: &str = DEFAULT_CONSOLE_CORS_ALLOWED_ORIGINS;
/// CORS allowed origins for the console service
/// Comma-separated list of origins or "*" for all origins
pub const ENV_CONSOLE_CORS_ALLOWED_ORIGINS: &str = "RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS";
/// Default CORS allowed origins for the console service
pub const DEFAULT_CONSOLE_CORS_ALLOWED_ORIGINS: &str = "*";
/// Enable or disable the console service
pub const ENV_CONSOLE_ENABLE: &str = "RUSTFS_CONSOLE_ENABLE";
/// Address for the console service to bind to
pub const ENV_CONSOLE_ADDRESS: &str = "RUSTFS_CONSOLE_ADDRESS";
/// RUSTFS_CONSOLE_RATE_LIMIT_ENABLE
/// Enable or disable rate limiting for the console service
pub const ENV_CONSOLE_RATE_LIMIT_ENABLE: &str = "RUSTFS_CONSOLE_RATE_LIMIT_ENABLE";
/// Default console rate limit enable
/// This is the default value for enabling rate limiting on the console server.
/// Rate limiting helps protect against abuse and DoS attacks on the management interface.
/// Default value: false
/// Environment variable: RUSTFS_CONSOLE_RATE_LIMIT_ENABLE
/// Command line argument: --console-rate-limit-enable
/// Example: RUSTFS_CONSOLE_RATE_LIMIT_ENABLE=true
/// Example: --console-rate-limit-enable true
pub const DEFAULT_CONSOLE_RATE_LIMIT_ENABLE: bool = false;
/// Set the rate limit requests per minute for the console service
/// Limits the number of requests per minute per client IP when rate limiting is enabled
/// Default: 100 requests per minute
pub const ENV_CONSOLE_RATE_LIMIT_RPM: &str = "RUSTFS_CONSOLE_RATE_LIMIT_RPM";
/// Default console rate limit requests per minute
/// This is the default rate limit for console requests when rate limiting is enabled.
/// Limits the number of requests per minute per client IP to prevent abuse.
/// Default value: 100 requests per minute
/// Environment variable: RUSTFS_CONSOLE_RATE_LIMIT_RPM
/// Command line argument: --console-rate-limit-rpm
/// Example: RUSTFS_CONSOLE_RATE_LIMIT_RPM=100
/// Example: --console-rate-limit-rpm 100
pub const DEFAULT_CONSOLE_RATE_LIMIT_RPM: u32 = 100;
/// Set the console authentication timeout in seconds
/// Specifies how long a console authentication session remains valid
/// Default: 3600 seconds (1 hour)
/// Minimum: 300 seconds (5 minutes)
/// Maximum: 86400 seconds (24 hours)
pub const ENV_CONSOLE_AUTH_TIMEOUT: &str = "RUSTFS_CONSOLE_AUTH_TIMEOUT";
/// Default console authentication timeout in seconds
/// This is the default timeout for console authentication sessions.
/// After this timeout, users need to re-authenticate to access the console.
/// Default value: 3600 seconds (1 hour)
/// Environment variable: RUSTFS_CONSOLE_AUTH_TIMEOUT
/// Command line argument: --console-auth-timeout
/// Example: RUSTFS_CONSOLE_AUTH_TIMEOUT=3600
/// Example: --console-auth-timeout 3600
pub const DEFAULT_CONSOLE_AUTH_TIMEOUT: u64 = 3600;

View File

@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
pub mod app;
pub mod env;
pub mod tls;
pub(crate) mod app;
pub(crate) mod console;
pub(crate) mod env;
pub(crate) mod tls;

View File

@@ -17,6 +17,8 @@ pub mod constants;
#[cfg(feature = "constants")]
pub use constants::app::*;
#[cfg(feature = "constants")]
pub use constants::console::*;
#[cfg(feature = "constants")]
pub use constants::env::*;
#[cfg(feature = "constants")]
pub use constants::tls::*;

View File

@@ -169,7 +169,7 @@ impl AsMut<Vec<Endpoints>> for PoolEndpointList {
impl PoolEndpointList {
/// creates a list of endpoints per pool, resolves their relevant
/// hostnames and discovers those are local or remote.
fn create_pool_endpoints(server_addr: &str, disks_layout: &DisksLayout) -> Result<Self> {
async fn create_pool_endpoints(server_addr: &str, disks_layout: &DisksLayout) -> Result<Self> {
if disks_layout.is_empty_layout() {
return Err(Error::other("invalid number of endpoints"));
}
@@ -244,7 +244,7 @@ impl PoolEndpointList {
let host_ip_set = if let Some(set) = host_ip_cache.get(&host) {
set
} else {
let ips = match get_host_ip(host.clone()) {
let ips = match get_host_ip(host.clone()).await {
Ok(ips) => ips,
Err(e) => {
error!("host {} not found, error:{}", host, e);
@@ -466,19 +466,22 @@ impl EndpointServerPools {
}
None
}
pub fn from_volumes(server_addr: &str, endpoints: Vec<String>) -> Result<(EndpointServerPools, SetupType)> {
pub async fn from_volumes(server_addr: &str, endpoints: Vec<String>) -> Result<(EndpointServerPools, SetupType)> {
let layouts = DisksLayout::from_volumes(endpoints.as_slice())?;
Self::create_server_endpoints(server_addr, &layouts)
Self::create_server_endpoints(server_addr, &layouts).await
}
/// validates and creates new endpoints from input args, supports
/// both ellipses and without ellipses transparently.
pub fn create_server_endpoints(server_addr: &str, disks_layout: &DisksLayout) -> Result<(EndpointServerPools, SetupType)> {
pub async fn create_server_endpoints(
server_addr: &str,
disks_layout: &DisksLayout,
) -> Result<(EndpointServerPools, SetupType)> {
if disks_layout.pools.is_empty() {
return Err(Error::other("Invalid arguments specified"));
}
let pool_eps = PoolEndpointList::create_pool_endpoints(server_addr, disks_layout)?;
let pool_eps = PoolEndpointList::create_pool_endpoints(server_addr, disks_layout).await?;
let mut ret: EndpointServerPools = Vec::with_capacity(pool_eps.as_ref().len()).into();
for (i, eps) in pool_eps.inner.into_iter().enumerate() {
@@ -753,8 +756,8 @@ mod test {
}
}
#[test]
fn test_create_pool_endpoints() {
#[tokio::test]
async fn test_create_pool_endpoints() {
#[derive(Default)]
struct TestCase<'a> {
num: usize,
@@ -1276,7 +1279,7 @@ mod test {
match (
test_case.expected_err,
PoolEndpointList::create_pool_endpoints(test_case.server_addr, &disks_layout),
PoolEndpointList::create_pool_endpoints(test_case.server_addr, &disks_layout).await,
) {
(None, Err(err)) => panic!("Test {}: error: expected = <nil>, got = {}", test_case.num, err),
(Some(err), Ok(_)) => panic!("Test {}: error: expected = {}, got = <nil>", test_case.num, err),
@@ -1343,8 +1346,8 @@ mod test {
(urls, local_flags)
}
#[test]
fn test_create_server_endpoints() {
#[tokio::test]
async fn test_create_server_endpoints() {
let test_cases = [
// Invalid input.
("", vec![], false),
@@ -1379,7 +1382,7 @@ mod test {
}
};
let ret = EndpointServerPools::create_server_endpoints(test_case.0, &disks_layout);
let ret = EndpointServerPools::create_server_endpoints(test_case.0, &disks_layout).await;
if let Err(err) = ret {
if test_case.2 {

View File

@@ -37,26 +37,27 @@ pub const DISK_FILL_FRACTION: f64 = 0.99;
pub const DISK_RESERVE_FRACTION: f64 = 0.15;
lazy_static! {
static ref GLOBAL_RUSTFS_PORT: OnceLock<u16> = OnceLock::new();
pub static ref GLOBAL_OBJECT_API: OnceLock<Arc<ECStore>> = OnceLock::new();
pub static ref GLOBAL_LOCAL_DISK: Arc<RwLock<Vec<Option<DiskStore>>>> = Arc::new(RwLock::new(Vec::new()));
pub static ref GLOBAL_IsErasure: RwLock<bool> = RwLock::new(false);
pub static ref GLOBAL_IsDistErasure: RwLock<bool> = RwLock::new(false);
pub static ref GLOBAL_IsErasureSD: RwLock<bool> = RwLock::new(false);
pub static ref GLOBAL_LOCAL_DISK_MAP: Arc<RwLock<HashMap<String, Option<DiskStore>>>> = Arc::new(RwLock::new(HashMap::new()));
pub static ref GLOBAL_LOCAL_DISK_SET_DRIVES: Arc<RwLock<TypeLocalDiskSetDrives>> = Arc::new(RwLock::new(Vec::new()));
pub static ref GLOBAL_Endpoints: OnceLock<EndpointServerPools> = OnceLock::new();
pub static ref GLOBAL_RootDiskThreshold: RwLock<u64> = RwLock::new(0);
pub static ref GLOBAL_TierConfigMgr: Arc<RwLock<TierConfigMgr>> = TierConfigMgr::new();
pub static ref GLOBAL_LifecycleSys: Arc<LifecycleSys> = LifecycleSys::new();
pub static ref GLOBAL_EventNotifier: Arc<RwLock<EventNotifier>> = EventNotifier::new();
//pub static ref GLOBAL_RemoteTargetTransport
static ref globalDeploymentIDPtr: OnceLock<Uuid> = OnceLock::new();
pub static ref GLOBAL_BOOT_TIME: OnceCell<SystemTime> = OnceCell::new();
pub static ref GLOBAL_LocalNodeName: String = "127.0.0.1:9000".to_string();
pub static ref GLOBAL_LocalNodeNameHex: String = rustfs_utils::crypto::hex(GLOBAL_LocalNodeName.as_bytes());
pub static ref GLOBAL_NodeNamesHex: HashMap<String, ()> = HashMap::new();
pub static ref GLOBAL_REGION: OnceLock<String> = OnceLock::new();
static ref GLOBAL_RUSTFS_PORT: OnceLock<u16> = OnceLock::new();
static ref GLOBAL_RUSTFS_EXTERNAL_PORT: OnceLock<u16> = OnceLock::new();
pub static ref GLOBAL_OBJECT_API: OnceLock<Arc<ECStore>> = OnceLock::new();
pub static ref GLOBAL_LOCAL_DISK: Arc<RwLock<Vec<Option<DiskStore>>>> = Arc::new(RwLock::new(Vec::new()));
pub static ref GLOBAL_IsErasure: RwLock<bool> = RwLock::new(false);
pub static ref GLOBAL_IsDistErasure: RwLock<bool> = RwLock::new(false);
pub static ref GLOBAL_IsErasureSD: RwLock<bool> = RwLock::new(false);
pub static ref GLOBAL_LOCAL_DISK_MAP: Arc<RwLock<HashMap<String, Option<DiskStore>>>> = Arc::new(RwLock::new(HashMap::new()));
pub static ref GLOBAL_LOCAL_DISK_SET_DRIVES: Arc<RwLock<TypeLocalDiskSetDrives>> = Arc::new(RwLock::new(Vec::new()));
pub static ref GLOBAL_Endpoints: OnceLock<EndpointServerPools> = OnceLock::new();
pub static ref GLOBAL_RootDiskThreshold: RwLock<u64> = RwLock::new(0);
pub static ref GLOBAL_TierConfigMgr: Arc<RwLock<TierConfigMgr>> = TierConfigMgr::new();
pub static ref GLOBAL_LifecycleSys: Arc<LifecycleSys> = LifecycleSys::new();
pub static ref GLOBAL_EventNotifier: Arc<RwLock<EventNotifier>> = EventNotifier::new();
//pub static ref GLOBAL_RemoteTargetTransport
static ref globalDeploymentIDPtr: OnceLock<Uuid> = OnceLock::new();
pub static ref GLOBAL_BOOT_TIME: OnceCell<SystemTime> = OnceCell::new();
pub static ref GLOBAL_LocalNodeName: String = "127.0.0.1:9000".to_string();
pub static ref GLOBAL_LocalNodeNameHex: String = rustfs_utils::crypto::hex(GLOBAL_LocalNodeName.as_bytes());
pub static ref GLOBAL_NodeNamesHex: HashMap<String, ()> = HashMap::new();
pub static ref GLOBAL_REGION: OnceLock<String> = OnceLock::new();
}
// Global cancellation token for background services (data scanner and auto heal)
@@ -108,6 +109,22 @@ pub fn set_global_rustfs_port(value: u16) {
GLOBAL_RUSTFS_PORT.set(value).expect("set_global_rustfs_port fail");
}
/// Get the global rustfs external port
pub fn global_rustfs_external_port() -> u16 {
if let Some(p) = GLOBAL_RUSTFS_EXTERNAL_PORT.get() {
*p
} else {
rustfs_config::DEFAULT_PORT
}
}
/// Set the global rustfs external port
pub fn set_global_rustfs_external_port(value: u16) {
GLOBAL_RUSTFS_EXTERNAL_PORT
.set(value)
.expect("set_global_rustfs_external_port fail");
}
/// Get the global rustfs port
pub fn set_global_deployment_id(id: Uuid) {
globalDeploymentIDPtr.set(id).unwrap();

View File

@@ -42,8 +42,8 @@ url.workspace = true
uuid.workspace = true
thiserror.workspace = true
once_cell.workspace = true
parking_lot = "0.12"
smallvec = "1.11"
smartstring = "1.0"
crossbeam-queue = "0.3"
heapless = "0.8"
parking_lot.workspace = true
smallvec.workspace = true
smartstring.workspace = true
crossbeam-queue = { workspace = true }
heapless = { workspace = true }

View File

@@ -616,6 +616,7 @@ impl ServerHandler for RustfsMcpServer {
server_info: Implementation {
name: "rustfs-mcp-server".into(),
version: env!("CARGO_PKG_VERSION").into(),
..Default::default()
},
}
}

View File

@@ -438,7 +438,7 @@ fn is_fatal_mqtt_error(err: &ConnectionError) -> bool {
rumqttc::StateError::InvalidState // The internal state machine is in invalid state
| rumqttc::StateError::WrongPacket // Agreement Violation: Unexpected Data Packet Received
| rumqttc::StateError::Unsolicited(_) // Agreement Violation: Unsolicited ACK Received
| rumqttc::StateError::OutgoingPacketTooLarge { .. } // Try to send too large packets
| rumqttc::StateError::CollisionTimeout // Agreement Violation (if this stage occurs)
| rumqttc::StateError::EmptySubscription // Agreement violation (if this stage occurs)
=> true,

View File

@@ -81,7 +81,7 @@ workspace = true
default = ["ip"] # features that are enabled by default
ip = ["dep:local-ip-address"] # ip characteristics and their dependencies
tls = ["dep:rustls", "dep:rustls-pemfile", "dep:rustls-pki-types"] # tls characteristics and their dependencies
net = ["ip", "dep:url", "dep:netif", "dep:futures", "dep:transform-stream", "dep:bytes", "dep:s3s", "dep:hyper", "dep:hyper-util", "dep:hickory-resolver", "dep:hickory-proto", "dep:moka", "dep:thiserror"] # network features with DNS resolver
net = ["ip", "dep:url", "dep:netif", "dep:futures", "dep:transform-stream", "dep:bytes", "dep:s3s", "dep:hyper", "dep:hyper-util", "dep:hickory-resolver", "dep:hickory-proto", "dep:moka", "dep:thiserror", "dep:tokio"] # network features with DNS resolver
io = ["dep:tokio"]
path = []
notify = ["dep:hyper", "dep:s3s"] # file system notification features

View File

@@ -127,6 +127,7 @@ impl LayeredDnsResolver {
/// Validate domain format according to RFC standards
#[instrument(skip_all, fields(domain = %domain))]
fn validate_domain_format(domain: &str) -> Result<(), DnsError> {
info!("Validating domain format start");
// Check FQDN length
if domain.len() > MAX_FQDN_LENGTH {
return Err(DnsError::InvalidFormat {
@@ -157,7 +158,7 @@ impl LayeredDnsResolver {
});
}
}
info!("DNS resolver validated successfully");
Ok(())
}
@@ -209,7 +210,6 @@ impl LayeredDnsResolver {
let ips: Vec<IpAddr> = lookup.iter().collect();
if !ips.is_empty() {
info!("System DNS resolution successful for domain: {} -> {} IPs", domain, ips.len());
debug!("System DNS resolved IPs: {:?}", ips);
Ok(ips)
} else {
warn!("System DNS returned empty result for domain: {}", domain);
@@ -242,7 +242,6 @@ impl LayeredDnsResolver {
let ips: Vec<IpAddr> = lookup.iter().collect();
if !ips.is_empty() {
info!("Public DNS resolution successful for domain: {} -> {} IPs", domain, ips.len());
debug!("Public DNS resolved IPs: {:?}", ips);
Ok(ips)
} else {
warn!("Public DNS returned empty result for domain: {}", domain);
@@ -270,6 +269,7 @@ impl LayeredDnsResolver {
/// 3. Public DNS (hickory-resolver with TLS-enabled Cloudflare DNS fallback)
#[instrument(skip_all, fields(domain = %domain))]
pub async fn resolve(&self, domain: &str) -> Result<Vec<IpAddr>, DnsError> {
info!("Starting DNS resolution process for domain: {} start", domain);
// Validate domain format first
Self::validate_domain_format(domain)?;
@@ -305,7 +305,7 @@ impl LayeredDnsResolver {
}
Err(public_err) => {
error!(
"All DNS resolution attempts failed for domain: {}. System DNS: failed, Public DNS: {}",
"All DNS resolution attempts failed for domain:` {}`. System DNS: failed, Public DNS: {}",
domain, public_err
);
Err(DnsError::AllAttemptsFailed {
@@ -345,6 +345,7 @@ pub fn get_global_dns_resolver() -> Option<&'static LayeredDnsResolver> {
/// Resolve domain using the global DNS resolver with comprehensive tracing
#[instrument(skip_all, fields(domain = %domain))]
pub async fn resolve_domain(domain: &str) -> Result<Vec<IpAddr>, DnsError> {
info!("resolving domain for: {}", domain);
match get_global_dns_resolver() {
Some(resolver) => resolver.resolve(domain).await,
None => Err(DnsError::InitializationFailed {

View File

@@ -15,6 +15,7 @@
use bytes::Bytes;
use futures::pin_mut;
use futures::{Stream, StreamExt};
use std::io::Error;
use std::net::Ipv6Addr;
use std::sync::{LazyLock, Mutex};
use std::{
@@ -23,6 +24,7 @@ use std::{
net::{IpAddr, SocketAddr, TcpListener, ToSocketAddrs},
time::{Duration, Instant},
};
use tracing::{error, info};
use transform_stream::AsyncTryStream;
use url::{Host, Url};
@@ -61,7 +63,7 @@ pub fn is_socket_addr(addr: &str) -> bool {
pub fn check_local_server_addr(server_addr: &str) -> std::io::Result<SocketAddr> {
let addr: Vec<SocketAddr> = match server_addr.to_socket_addrs() {
Ok(addr) => addr.collect(),
Err(err) => return Err(std::io::Error::other(err)),
Err(err) => return Err(Error::other(err)),
};
// 0.0.0.0 is a wildcard address and refers to local network
@@ -82,7 +84,7 @@ pub fn check_local_server_addr(server_addr: &str) -> std::io::Result<SocketAddr>
}
}
Err(std::io::Error::other("host in server address should be this server"))
Err(Error::other("host in server address should be this server"))
}
/// checks if the given parameter correspond to one of
@@ -93,7 +95,7 @@ pub fn is_local_host(host: Host<&str>, port: u16, local_port: u16) -> std::io::R
Host::Domain(domain) => {
let ips = match (domain, 0).to_socket_addrs().map(|v| v.map(|v| v.ip()).collect::<Vec<_>>()) {
Ok(ips) => ips,
Err(err) => return Err(std::io::Error::other(err)),
Err(err) => return Err(Error::other(err)),
};
ips.iter().any(|ip| local_set.contains(ip))
@@ -113,49 +115,20 @@ pub fn is_local_host(host: Host<&str>, port: u16, local_port: u16) -> std::io::R
///
/// This is the async version of `get_host_ip()` that provides enhanced DNS resolution
/// with Kubernetes support when the "net" feature is enabled.
pub async fn get_host_ip_async(host: Host<&str>) -> std::io::Result<HashSet<IpAddr>> {
pub async fn get_host_ip(host: Host<&str>) -> std::io::Result<HashSet<IpAddr>> {
match host {
Host::Domain(domain) => {
#[cfg(feature = "net")]
{
use crate::dns_resolver::resolve_domain;
match resolve_domain(domain).await {
Ok(ips) => Ok(ips.into_iter().collect()),
Err(e) => Err(std::io::Error::other(format!("DNS resolution failed: {}", e))),
match crate::dns_resolver::resolve_domain(domain).await {
Ok(ips) => {
info!("Resolved domain {domain} using custom DNS resolver: {ips:?}");
return Ok(ips.into_iter().collect());
}
Err(err) => {
error!(
"Failed to resolve domain {domain} using custom DNS resolver, falling back to system resolver,err: {err}"
);
}
}
#[cfg(not(feature = "net"))]
{
// Fallback to standard resolution when DNS resolver is not available
match (domain, 0)
.to_socket_addrs()
.map(|v| v.map(|v| v.ip()).collect::<HashSet<_>>())
{
Ok(ips) => Ok(ips),
Err(err) => Err(std::io::Error::other(err)),
}
}
}
Host::Ipv4(ip) => {
let mut set = HashSet::with_capacity(1);
set.insert(IpAddr::V4(ip));
Ok(set)
}
Host::Ipv6(ip) => {
let mut set = HashSet::with_capacity(1);
set.insert(IpAddr::V6(ip));
Ok(set)
}
}
}
/// returns IP address of given host using standard resolution.
///
/// **Note**: This function uses standard library DNS resolution with caching.
/// For enhanced DNS resolution with Kubernetes support, use `get_host_ip_async()`.
pub fn get_host_ip(host: Host<&str>) -> std::io::Result<HashSet<IpAddr>> {
match host {
Host::Domain(domain) => {
// Check cache first
if let Ok(mut cache) = DNS_CACHE.lock() {
if let Some(entry) = cache.get(domain) {
@@ -167,7 +140,9 @@ pub fn get_host_ip(host: Host<&str>) -> std::io::Result<HashSet<IpAddr>> {
}
}
// Perform DNS resolution
info!("Cache miss for domain {domain}, querying system resolver.");
// Fallback to standard resolution when DNS resolver is not available
match (domain, 0)
.to_socket_addrs()
.map(|v| v.map(|v| v.ip()).collect::<HashSet<_>>())
@@ -181,21 +156,17 @@ pub fn get_host_ip(host: Host<&str>) -> std::io::Result<HashSet<IpAddr>> {
cache.retain(|_, v| !v.is_expired(DNS_CACHE_TTL));
}
}
info!("System query for domain {domain}: {:?}", ips);
Ok(ips)
}
Err(err) => Err(std::io::Error::other(err)),
Err(err) => {
error!("Failed to resolve domain {domain} using system resolver, err: {err}");
Err(Error::other(err))
}
}
}
Host::Ipv4(ip) => {
let mut set = HashSet::with_capacity(1);
set.insert(IpAddr::V4(ip));
Ok(set)
}
Host::Ipv6(ip) => {
let mut set = HashSet::with_capacity(1);
set.insert(IpAddr::V6(ip));
Ok(set)
}
Host::Ipv4(ip) => Ok([IpAddr::V4(ip)].into_iter().collect()),
Host::Ipv6(ip) => Ok([IpAddr::V6(ip)].into_iter().collect()),
}
}
@@ -207,7 +178,7 @@ pub fn get_available_port() -> u16 {
pub fn must_get_local_ips() -> std::io::Result<Vec<IpAddr>> {
match netif::up() {
Ok(up) => Ok(up.map(|x| x.address().to_owned()).collect()),
Err(err) => Err(std::io::Error::other(format!("Unable to get IP addresses of this host: {err}"))),
Err(err) => Err(Error::other(format!("Unable to get IP addresses of this host: {err}"))),
}
}
@@ -215,7 +186,7 @@ pub fn get_default_location(_u: Url, _region_override: &str) -> String {
todo!();
}
pub fn get_endpoint_url(endpoint: &str, secure: bool) -> Result<Url, std::io::Error> {
pub fn get_endpoint_url(endpoint: &str, secure: bool) -> Result<Url, Error> {
let mut scheme = "https";
if !secure {
scheme = "http";
@@ -223,7 +194,7 @@ pub fn get_endpoint_url(endpoint: &str, secure: bool) -> Result<Url, std::io::Er
let endpoint_url_str = format!("{scheme}://{endpoint}");
let Ok(endpoint_url) = Url::parse(&endpoint_url_str) else {
return Err(std::io::Error::other("url parse error."));
return Err(Error::other("url parse error."));
};
//is_valid_endpoint_url(endpoint_url)?;
@@ -258,7 +229,7 @@ impl Display for XHost {
}
impl TryFrom<String> for XHost {
type Error = std::io::Error;
type Error = Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
if let Some(addr) = value.to_socket_addrs()?.next() {
@@ -268,7 +239,7 @@ impl TryFrom<String> for XHost {
is_port_set: addr.port() > 0,
})
} else {
Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "value invalid"))
Err(Error::new(std::io::ErrorKind::InvalidData, "value invalid"))
}
}
}
@@ -278,7 +249,7 @@ pub fn parse_and_resolve_address(addr_str: &str) -> std::io::Result<SocketAddr>
let port_str = port;
let port: u16 = port_str
.parse()
.map_err(|e| std::io::Error::other(format!("Invalid port format: {addr_str}, err:{e:?}")))?;
.map_err(|e| Error::other(format!("Invalid port format: {addr_str}, err:{e:?}")))?;
let final_port = if port == 0 {
get_available_port() // assume get_available_port is available here
} else {
@@ -318,9 +289,9 @@ where
#[cfg(test)]
mod test {
use std::net::{Ipv4Addr, Ipv6Addr};
use super::*;
use crate::init_global_dns_resolver;
use std::net::{Ipv4Addr, Ipv6Addr};
#[test]
fn test_is_socket_addr() {
@@ -424,23 +395,29 @@ mod test {
assert!(is_local_host(invalid_host, 0, 0).is_err());
}
#[test]
fn test_get_host_ip() {
#[tokio::test]
async fn test_get_host_ip() {
match init_global_dns_resolver().await {
Ok(_) => {}
Err(e) => {
error!("Failed to initialize global DNS resolver: {e}");
}
}
// Test IPv4 address
let ipv4_host = Host::Ipv4(Ipv4Addr::new(192, 168, 1, 1));
let ipv4_result = get_host_ip(ipv4_host).unwrap();
let ipv4_result = get_host_ip(ipv4_host).await.unwrap();
assert_eq!(ipv4_result.len(), 1);
assert!(ipv4_result.contains(&IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))));
// Test IPv6 address
let ipv6_host = Host::Ipv6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1));
let ipv6_result = get_host_ip(ipv6_host).unwrap();
let ipv6_result = get_host_ip(ipv6_host).await.unwrap();
assert_eq!(ipv6_result.len(), 1);
assert!(ipv6_result.contains(&IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1))));
// Test localhost domain
let localhost_host = Host::Domain("localhost");
let localhost_result = get_host_ip(localhost_host).unwrap();
let localhost_result = get_host_ip(localhost_host).await.unwrap();
assert!(!localhost_result.is_empty());
// Should contain at least loopback address
assert!(
@@ -450,7 +427,16 @@ mod test {
// Test invalid domain
let invalid_host = Host::Domain("invalid.nonexistent.domain.example");
assert!(get_host_ip(invalid_host).is_err());
match get_host_ip(invalid_host.clone()).await {
Ok(ips) => {
// Depending on DNS resolver behavior, it might return empty set or error
assert!(ips.is_empty(), "Expected empty IP set for invalid domain, got: {:?}", ips);
}
Err(_) => {
error!("Expected error for invalid domain");
} // Expected error
}
assert!(get_host_ip(invalid_host).await.is_err());
}
#[test]

View File

@@ -28,10 +28,15 @@ services:
TARGETPLATFORM: linux/amd64
ports:
- "9000:9000" # S3 API port
- "9001:9001" # Console port
environment:
- RUSTFS_VOLUMES=/data/rustfs0,/data/rustfs1,/data/rustfs2,/data/rustfs3
- RUSTFS_ADDRESS=0.0.0.0:9000
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_EXTERNAL_ADDRESS=:9000 # Same as internal since no port mapping
- RUSTFS_CORS_ALLOWED_ORIGINS=*
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
- RUSTFS_ACCESS_KEY=rustfsadmin
- RUSTFS_SECRET_KEY=rustfsadmin
- RUSTFS_LOG_LEVEL=info
@@ -41,7 +46,7 @@ services:
- rustfs_data_1:/data/rustfs1
- rustfs_data_2:/data/rustfs2
- rustfs_data_3:/data/rustfs3
- ./logs:/app/logs
- logs_data:/app/logs
networks:
- rustfs-network
restart: unless-stopped
@@ -49,11 +54,8 @@ services:
test:
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://localhost:9000/health",
"sh", "-c",
"curl -f http://localhost:9000/health && curl -f http://localhost:9001/health"
]
interval: 30s
timeout: 10s
@@ -71,11 +73,16 @@ services:
dockerfile: Dockerfile.source
# Pure development environment
ports:
- "9010:9000"
- "9010:9000" # S3 API port
- "9011:9001" # Console port
environment:
- RUSTFS_VOLUMES=/data/rustfs0,/data/rustfs1
- RUSTFS_ADDRESS=0.0.0.0:9000
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
- RUSTFS_CONSOLE_ENABLE=true
- RUSTFS_EXTERNAL_ADDRESS=:9010 # External port mapping 9010 -> 9000
- RUSTFS_CORS_ALLOWED_ORIGINS=*
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
- RUSTFS_ACCESS_KEY=devadmin
- RUSTFS_SECRET_KEY=devadmin
- RUSTFS_LOG_LEVEL=debug
@@ -85,6 +92,17 @@ services:
networks:
- rustfs-network
restart: unless-stopped
healthcheck:
test:
[
"CMD",
"sh", "-c",
"curl -f http://localhost:9000/health && curl -f http://localhost:9001/health"
]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
profiles:
- dev
@@ -95,7 +113,7 @@ services:
command:
- --config=/etc/otelcol-contrib/otel-collector.yml
volumes:
- ./.docker/observability/otel-collector.yml:/etc/otelcol-contrib/otel-collector.yml:ro
- ./.docker/observability/otel-collector-config.yaml:/etc/otelcol-contrib/otel-collector.yml:ro
ports:
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
@@ -219,3 +237,5 @@ volumes:
driver: local
redis_data:
driver: local
logs_data:
driver: local

1362
docs/console-separation.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -56,4 +56,5 @@ if [ "${RUSTFS_ACCESS_KEY}" = "rustfsadmin" ] || [ "${RUSTFS_SECRET_KEY}" = "rus
fi
echo "Starting: $*"
set -- "$@" $LOCAL_VOLUMES
exec "$@"

270
examples/README.md Normal file
View File

@@ -0,0 +1,270 @@
# RustFS Docker Deployment Examples
This directory contains various deployment scripts and configuration files for RustFS with console and endpoint service separation.
## Quick Start Scripts
### `docker-quickstart.sh`
The fastest way to get RustFS running with different configurations.
```bash
# Basic deployment (ports 9000-9001)
./docker-quickstart.sh basic
# Development environment (ports 9010-9011)
./docker-quickstart.sh dev
# Production-like deployment (ports 9020-9021)
./docker-quickstart.sh prod
# Check status of all deployments
./docker-quickstart.sh status
# Test health of all running services
./docker-quickstart.sh test
# Clean up all containers
./docker-quickstart.sh cleanup
```
### `enhanced-docker-deployment.sh`
Comprehensive deployment script with multiple scenarios and detailed logging.
```bash
# Deploy individual scenarios
./enhanced-docker-deployment.sh basic # Basic setup with port mapping
./enhanced-docker-deployment.sh dev # Development environment
./enhanced-docker-deployment.sh prod # Production-like with security
# Deploy all scenarios at once
./enhanced-docker-deployment.sh all
# Check status and test services
./enhanced-docker-deployment.sh status
./enhanced-docker-deployment.sh test
# View logs for specific container
./enhanced-docker-deployment.sh logs rustfs-dev
# Complete cleanup
./enhanced-docker-deployment.sh cleanup
```
### `enhanced-security-deployment.sh`
Production-ready deployment with enhanced security features including TLS, rate limiting, and secure credential generation.
```bash
# Deploy with security hardening
./enhanced-security-deployment.sh
# Features:
# - Automatic TLS certificate generation
# - Secure credential generation
# - Rate limiting configuration
# - Console access restrictions
# - Health check validation
```
## Docker Compose Examples
### `docker-comprehensive.yml`
Complete Docker Compose configuration with multiple deployment profiles.
```bash
# Deploy specific profiles
docker-compose -f docker-comprehensive.yml --profile basic up -d
docker-compose -f docker-comprehensive.yml --profile dev up -d
docker-compose -f docker-comprehensive.yml --profile production up -d
docker-compose -f docker-comprehensive.yml --profile enterprise up -d
docker-compose -f docker-comprehensive.yml --profile api-only up -d
# Deploy with reverse proxy
docker-compose -f docker-comprehensive.yml --profile production --profile nginx up -d
```
#### Available Profiles:
- **basic**: Simple deployment for testing (ports 9000-9001)
- **dev**: Development environment with debug logging (ports 9010-9011)
- **production**: Production deployment with security (ports 9020-9021)
- **enterprise**: Full enterprise setup with TLS (ports 9030-9443)
- **api-only**: API endpoint without console (port 9040)
## Usage Examples by Scenario
### Development Setup
```bash
# Quick development start
./docker-quickstart.sh dev
# Or use enhanced deployment for more features
./enhanced-docker-deployment.sh dev
# Or use Docker Compose
docker-compose -f docker-comprehensive.yml --profile dev up -d
```
**Access Points:**
- API: http://localhost:9010 (or 9030 for enhanced)
- Console: http://localhost:9011/rustfs/console/ (or 9031 for enhanced)
- Credentials: dev-admin / dev-secret
### Production Deployment
```bash
# Security-hardened deployment
./enhanced-security-deployment.sh
# Or production profile
./enhanced-docker-deployment.sh prod
```
**Features:**
- TLS encryption for console
- Rate limiting enabled
- Restricted CORS policies
- Secure credential generation
- Console bound to localhost only
### Testing and CI/CD
```bash
# API-only deployment for testing
docker-compose -f docker-comprehensive.yml --profile api-only up -d
# Quick basic setup for integration tests
./docker-quickstart.sh basic
```
## Configuration Examples
### Environment Variables
All deployment scripts support customization via environment variables:
```bash
# Custom image and ports
export RUSTFS_IMAGE="rustfs/rustfs:custom-tag"
export CONSOLE_PORT="8001"
export API_PORT="8000"
# Custom data directories
export DATA_DIR="/custom/data/path"
export CERTS_DIR="/custom/certs/path"
# Run with custom configuration
./enhanced-security-deployment.sh
```
### Common Configurations
```bash
# Development - permissive CORS
RUSTFS_CORS_ALLOWED_ORIGINS="*"
RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="*"
# Production - restrictive CORS
RUSTFS_CORS_ALLOWED_ORIGINS="https://myapp.com,https://api.myapp.com"
RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="https://admin.myapp.com"
# Security hardening
RUSTFS_CONSOLE_RATE_LIMIT_ENABLE="true"
RUSTFS_CONSOLE_RATE_LIMIT_RPM="60"
RUSTFS_CONSOLE_AUTH_TIMEOUT="1800"
```
## Monitoring and Health Checks
All deployments include health check endpoints:
```bash
# Test API health
curl http://localhost:9000/health
# Test console health
curl http://localhost:9001/health
# Test all deployments
./docker-quickstart.sh test
./enhanced-docker-deployment.sh test
```
## Network Architecture
### Port Mappings
| Deployment | API Port | Console Port | Description |
|-----------|----------|--------------|-------------|
| Basic | 9000 | 9001 | Simple deployment |
| Dev | 9010 | 9011 | Development environment |
| Prod | 9020 | 9021 | Production-like setup |
| Enterprise | 9030 | 9443 | Enterprise with TLS |
| API-Only | 9040 | - | API endpoint only |
### Network Isolation
Production deployments use network isolation:
- **Public API Network**: Exposes API endpoints to external clients
- **Internal Console Network**: Restricts console access to internal networks
- **Secure Network**: Isolated network for enterprise deployments
## Security Considerations
### Development
- Permissive CORS policies for easy testing
- Debug logging enabled
- Default credentials for simplicity
### Production
- Restrictive CORS policies
- TLS encryption for console
- Rate limiting enabled
- Secure credential generation
- Console bound to localhost
- Network isolation
### Enterprise
- Complete TLS encryption
- Advanced rate limiting
- Authentication timeouts
- Secret management
- Network segregation
## Troubleshooting
### Common Issues
1. **Port Conflicts**: Use different ports via environment variables
2. **CORS Errors**: Check origin configuration and browser network tab
3. **Health Check Failures**: Verify services are running and ports are accessible
4. **Permission Issues**: Check volume mount permissions and certificate file permissions
### Debug Commands
```bash
# Check container logs
docker logs rustfs-container
# Check container environment
docker exec rustfs-container env | grep RUSTFS
# Test connectivity
docker exec rustfs-container curl http://localhost:9000/health
docker exec rustfs-container curl http://localhost:9001/health
# Check listening ports
docker exec rustfs-container netstat -tulpn | grep -E ':(9000|9001)'
```
## Migration from Previous Versions
See [docs/console-separation.md](../docs/console-separation.md) for detailed migration instructions from single-port deployments to the separated architecture.
## Additional Resources
- [Console Separation Documentation](../docs/console-separation.md)
- [Docker Compose Configuration](../docker-compose.yml)
- [Main Dockerfile](../Dockerfile)
- [Security Best Practices](../docs/console-separation.md#security-hardening)

View File

@@ -0,0 +1,224 @@
# RustFS Comprehensive Docker Deployment Examples
# This file demonstrates various deployment scenarios for RustFS with console separation
version: "3.8"
services:
# Basic deployment with default settings
rustfs-basic:
image: rustfs/rustfs:latest
container_name: rustfs-basic
ports:
- "9000:9000" # API endpoint
- "9001:9001" # Console interface
environment:
- RUSTFS_ADDRESS=0.0.0.0:9000
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
- RUSTFS_EXTERNAL_ADDRESS=:9000
- RUSTFS_CORS_ALLOWED_ORIGINS=http://localhost:9001
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
- RUSTFS_ACCESS_KEY=admin
- RUSTFS_SECRET_KEY=password
volumes:
- rustfs-basic-data:/data
networks:
- rustfs-network
restart: unless-stopped
healthcheck:
test: ["CMD", "sh", "-c", "curl -f http://localhost:9000/health && curl -f http://localhost:9001/health"]
interval: 30s
timeout: 10s
retries: 3
profiles:
- basic
# Development environment with debug logging
rustfs-dev:
image: rustfs/rustfs:latest
container_name: rustfs-dev
ports:
- "9010:9000" # API endpoint
- "9011:9001" # Console interface
environment:
- RUSTFS_ADDRESS=0.0.0.0:9000
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
- RUSTFS_EXTERNAL_ADDRESS=:9010
- RUSTFS_CORS_ALLOWED_ORIGINS=*
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
- RUSTFS_ACCESS_KEY=dev-admin
- RUSTFS_SECRET_KEY=dev-password
- RUST_LOG=debug
- RUSTFS_LOG_LEVEL=debug
volumes:
- rustfs-dev-data:/data
- rustfs-dev-logs:/logs
networks:
- rustfs-network
restart: unless-stopped
healthcheck:
test: ["CMD", "sh", "-c", "curl -f http://localhost:9000/health && curl -f http://localhost:9001/health"]
interval: 30s
timeout: 10s
retries: 3
profiles:
- dev
# Production environment with security hardening
rustfs-production:
image: rustfs/rustfs:latest
container_name: rustfs-production
ports:
- "9020:9000" # API endpoint (public)
- "127.0.0.1:9021:9001" # Console (localhost only)
environment:
- RUSTFS_ADDRESS=0.0.0.0:9000
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
- RUSTFS_EXTERNAL_ADDRESS=:9020
- RUSTFS_CORS_ALLOWED_ORIGINS=https://myapp.com,https://api.myapp.com
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=https://admin.myapp.com
- RUSTFS_CONSOLE_RATE_LIMIT_ENABLE=true
- RUSTFS_CONSOLE_RATE_LIMIT_RPM=60
- RUSTFS_CONSOLE_AUTH_TIMEOUT=1800
- RUSTFS_ACCESS_KEY_FILE=/run/secrets/rustfs_access_key
- RUSTFS_SECRET_KEY_FILE=/run/secrets/rustfs_secret_key
volumes:
- rustfs-production-data:/data
- rustfs-production-logs:/logs
- rustfs-certs:/certs:ro
networks:
- rustfs-network
secrets:
- rustfs_access_key
- rustfs_secret_key
restart: unless-stopped
healthcheck:
test: ["CMD", "sh", "-c", "curl -f http://localhost:9000/health && curl -f http://localhost:9001/health"]
interval: 30s
timeout: 10s
retries: 3
profiles:
- production
# Enterprise deployment with TLS and full security
rustfs-enterprise:
image: rustfs/rustfs:latest
container_name: rustfs-enterprise
ports:
- "9030:9000" # API endpoint
- "127.0.0.1:9443:9001" # Console with TLS (localhost only)
environment:
- RUSTFS_ADDRESS=0.0.0.0:9000
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
- RUSTFS_EXTERNAL_ADDRESS=:9030
- RUSTFS_TLS_PATH=/certs
- RUSTFS_CORS_ALLOWED_ORIGINS=https://enterprise.com
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=https://admin.enterprise.com
- RUSTFS_CONSOLE_RATE_LIMIT_ENABLE=true
- RUSTFS_CONSOLE_RATE_LIMIT_RPM=30
- RUSTFS_CONSOLE_AUTH_TIMEOUT=900
volumes:
- rustfs-enterprise-data:/data
- rustfs-enterprise-logs:/logs
- rustfs-enterprise-certs:/certs:ro
networks:
- rustfs-secure-network
secrets:
- rustfs_enterprise_access_key
- rustfs_enterprise_secret_key
restart: unless-stopped
healthcheck:
test: ["CMD", "sh", "-c", "curl -f http://localhost:9000/health && curl -k -f https://localhost:9001/health"]
interval: 30s
timeout: 10s
retries: 3
profiles:
- enterprise
# API-only deployment (console disabled)
rustfs-api-only:
image: rustfs/rustfs:latest
container_name: rustfs-api-only
ports:
- "9040:9000" # API endpoint only
environment:
- RUSTFS_ADDRESS=0.0.0.0:9000
- RUSTFS_CONSOLE_ENABLE=false
- RUSTFS_CORS_ALLOWED_ORIGINS=https://client-app.com
- RUSTFS_ACCESS_KEY=api-only-key
- RUSTFS_SECRET_KEY=api-only-secret
volumes:
- rustfs-api-data:/data
networks:
- rustfs-network
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/health"]
interval: 30s
timeout: 10s
retries: 3
profiles:
- api-only
# Nginx reverse proxy for production
nginx-proxy:
image: nginx:alpine
container_name: rustfs-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
networks:
- rustfs-network
restart: unless-stopped
depends_on:
- rustfs-production
profiles:
- production
- enterprise
networks:
rustfs-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
rustfs-secure-network:
driver: bridge
internal: true
ipam:
config:
- subnet: 172.21.0.0/16
volumes:
rustfs-basic-data:
driver: local
rustfs-dev-data:
driver: local
rustfs-dev-logs:
driver: local
rustfs-production-data:
driver: local
rustfs-production-logs:
driver: local
rustfs-enterprise-data:
driver: local
rustfs-enterprise-logs:
driver: local
rustfs-enterprise-certs:
driver: local
rustfs-api-data:
driver: local
rustfs-certs:
driver: local
secrets:
rustfs_access_key:
external: true
rustfs_secret_key:
external: true
rustfs_enterprise_access_key:
external: true
rustfs_enterprise_secret_key:
external: true

295
examples/docker-quickstart.sh Executable file
View File

@@ -0,0 +1,295 @@
#!/bin/bash
# RustFS Docker Quick Start Script
# This script provides easy deployment commands for different scenarios
set -e
# Colors for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
log() {
echo -e "${GREEN}[RustFS]${NC} $1"
}
info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Print banner
print_banner() {
echo -e "${BLUE}"
echo "=================================================="
echo " RustFS Docker Quick Start"
echo " Console & Endpoint Separation"
echo "=================================================="
echo -e "${NC}"
}
# Check Docker availability
check_docker() {
if ! command -v docker &> /dev/null; then
error "Docker is not installed or not available in PATH"
exit 1
fi
info "Docker is available: $(docker --version)"
}
# Quick start - basic deployment
quick_basic() {
log "Starting RustFS basic deployment..."
docker run -d \
--name rustfs-quick \
-p 9000:9000 \
-p 9001:9001 \
-e RUSTFS_EXTERNAL_ADDRESS=":9000" \
-e RUSTFS_CORS_ALLOWED_ORIGINS="http://localhost:9001" \
-v rustfs-quick-data:/data \
rustfs/rustfs:latest
echo
info "✅ RustFS deployed successfully!"
info "🌐 API Endpoint: http://localhost:9000"
info "🖥️ Console UI: http://localhost:9001/rustfs/console/"
info "🔐 Credentials: rustfsadmin / rustfsadmin"
info "🏥 Health Check: curl http://localhost:9000/health"
echo
info "To stop: docker stop rustfs-quick"
info "To remove: docker rm rustfs-quick && docker volume rm rustfs-quick-data"
}
# Development deployment with debug logging
quick_dev() {
log "Starting RustFS development environment..."
docker run -d \
--name rustfs-dev \
-p 9010:9000 \
-p 9011:9001 \
-e RUSTFS_EXTERNAL_ADDRESS=":9010" \
-e RUSTFS_CORS_ALLOWED_ORIGINS="*" \
-e RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="*" \
-e RUSTFS_ACCESS_KEY="dev-admin" \
-e RUSTFS_SECRET_KEY="dev-secret" \
-e RUST_LOG="debug" \
-v rustfs-dev-data:/data \
rustfs/rustfs:latest
echo
info "✅ RustFS development environment ready!"
info "🌐 API Endpoint: http://localhost:9010"
info "🖥️ Console UI: http://localhost:9011/rustfs/console/"
info "🔐 Credentials: dev-admin / dev-secret"
info "📊 Debug logging enabled"
echo
info "To stop: docker stop rustfs-dev"
}
# Production-like deployment
quick_prod() {
log "Starting RustFS production-like deployment..."
# Generate secure credentials
ACCESS_KEY="prod-$(openssl rand -hex 8)"
SECRET_KEY="$(openssl rand -hex 24)"
docker run -d \
--name rustfs-prod \
-p 9020:9000 \
-p 127.0.0.1:9021:9001 \
-e RUSTFS_EXTERNAL_ADDRESS=":9020" \
-e RUSTFS_CORS_ALLOWED_ORIGINS="https://myapp.com" \
-e RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="https://admin.myapp.com" \
-e RUSTFS_CONSOLE_RATE_LIMIT_ENABLE="true" \
-e RUSTFS_CONSOLE_RATE_LIMIT_RPM="60" \
-e RUSTFS_ACCESS_KEY="$ACCESS_KEY" \
-e RUSTFS_SECRET_KEY="$SECRET_KEY" \
-v rustfs-prod-data:/data \
rustfs/rustfs:latest
# Save credentials
echo "RUSTFS_ACCESS_KEY=$ACCESS_KEY" > rustfs-prod-credentials.txt
echo "RUSTFS_SECRET_KEY=$SECRET_KEY" >> rustfs-prod-credentials.txt
chmod 600 rustfs-prod-credentials.txt
echo
info "✅ RustFS production deployment ready!"
info "🌐 API Endpoint: http://localhost:9020 (public)"
info "🖥️ Console UI: http://127.0.0.1:9021/rustfs/console/ (localhost only)"
info "🔐 Credentials saved to rustfs-prod-credentials.txt"
info "🔒 Console restricted to localhost for security"
echo
warn "⚠️ Change default CORS origins for production use"
}
# Stop and cleanup
cleanup() {
log "Cleaning up RustFS deployments..."
docker stop rustfs-quick rustfs-dev rustfs-prod 2>/dev/null || true
docker rm rustfs-quick rustfs-dev rustfs-prod 2>/dev/null || true
info "Containers stopped and removed"
echo
info "To also remove data volumes, run:"
info "docker volume rm rustfs-quick-data rustfs-dev-data rustfs-prod-data"
}
# Show status of all deployments
status() {
log "RustFS deployment status:"
echo
if docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -q rustfs; then
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | head -n1
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep rustfs
else
info "No RustFS containers are currently running"
fi
echo
info "Available endpoints:"
if docker ps --filter "name=rustfs-quick" --format "{{.Names}}" | grep -q rustfs-quick; then
echo " Basic: http://localhost:9000 (API) | http://localhost:9001/rustfs/console/ (Console)"
fi
if docker ps --filter "name=rustfs-dev" --format "{{.Names}}" | grep -q rustfs-dev; then
echo " Dev: http://localhost:9010 (API) | http://localhost:9011/rustfs/console/ (Console)"
fi
if docker ps --filter "name=rustfs-prod" --format "{{.Names}}" | grep -q rustfs-prod; then
echo " Prod: http://localhost:9020 (API) | http://127.0.0.1:9021/rustfs/console/ (Console)"
fi
}
# Test deployments
test_deployments() {
log "Testing RustFS deployments..."
echo
# Test basic deployment
if docker ps --filter "name=rustfs-quick" --format "{{.Names}}" | grep -q rustfs-quick; then
info "Testing basic deployment..."
if curl -s -f http://localhost:9000/health | grep -q "ok"; then
echo " ✅ API health check: PASS"
else
echo " ❌ API health check: FAIL"
fi
if curl -s -f http://localhost:9001/health | grep -q "console"; then
echo " ✅ Console health check: PASS"
else
echo " ❌ Console health check: FAIL"
fi
fi
# Test dev deployment
if docker ps --filter "name=rustfs-dev" --format "{{.Names}}" | grep -q rustfs-dev; then
info "Testing development deployment..."
if curl -s -f http://localhost:9010/health | grep -q "ok"; then
echo " ✅ Dev API health check: PASS"
else
echo " ❌ Dev API health check: FAIL"
fi
if curl -s -f http://localhost:9011/health | grep -q "console"; then
echo " ✅ Dev Console health check: PASS"
else
echo " ❌ Dev Console health check: FAIL"
fi
fi
# Test prod deployment
if docker ps --filter "name=rustfs-prod" --format "{{.Names}}" | grep -q rustfs-prod; then
info "Testing production deployment..."
if curl -s -f http://localhost:9020/health | grep -q "ok"; then
echo " ✅ Prod API health check: PASS"
else
echo " ❌ Prod API health check: FAIL"
fi
if curl -s -f http://127.0.0.1:9021/health | grep -q "console"; then
echo " ✅ Prod Console health check: PASS"
else
echo " ❌ Prod Console health check: FAIL"
fi
fi
}
# Show help
show_help() {
print_banner
echo "Usage: $0 [command]"
echo
echo "Commands:"
echo " basic Start basic RustFS deployment (ports 9000-9001)"
echo " dev Start development deployment with debug logging (ports 9010-9011)"
echo " prod Start production-like deployment with security (ports 9020-9021)"
echo " status Show status of running deployments"
echo " test Test health of all running deployments"
echo " cleanup Stop and remove all RustFS containers"
echo " help Show this help message"
echo
echo "Examples:"
echo " $0 basic # Quick start with default settings"
echo " $0 dev # Development environment with debug logs"
echo " $0 prod # Production-like setup with security"
echo " $0 status # Check what's running"
echo " $0 test # Test all deployments"
echo " $0 cleanup # Clean everything up"
echo
echo "For more advanced deployments, see:"
echo " - examples/enhanced-docker-deployment.sh"
echo " - examples/enhanced-security-deployment.sh"
echo " - examples/docker-comprehensive.yml"
echo " - docs/console-separation.md"
echo
}
# Main execution
case "${1:-help}" in
"basic")
print_banner
check_docker
quick_basic
;;
"dev")
print_banner
check_docker
quick_dev
;;
"prod")
print_banner
check_docker
quick_prod
;;
"status")
print_banner
status
;;
"test")
print_banner
test_deployments
;;
"cleanup")
print_banner
cleanup
;;
"help"|*)
show_help
;;
esac

View File

@@ -0,0 +1,321 @@
#!/bin/bash
# RustFS Enhanced Docker Deployment Examples
# This script demonstrates various deployment scenarios for RustFS with console separation
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_section() {
echo -e "\n${BLUE}========================================${NC}"
echo -e "${BLUE}$1${NC}"
echo -e "${BLUE}========================================${NC}\n"
}
# Function to clean up existing containers
cleanup() {
log_info "Cleaning up existing RustFS containers..."
docker stop rustfs-basic rustfs-dev rustfs-prod 2>/dev/null || true
docker rm rustfs-basic rustfs-dev rustfs-prod 2>/dev/null || true
}
# Function to wait for service to be ready
wait_for_service() {
local url=$1
local service_name=$2
local max_attempts=30
local attempt=0
log_info "Waiting for $service_name to be ready at $url..."
while [ $attempt -lt $max_attempts ]; do
if curl -s -f "$url" > /dev/null 2>&1; then
log_info "$service_name is ready!"
return 0
fi
attempt=$((attempt + 1))
sleep 1
done
log_error "$service_name failed to start within ${max_attempts}s"
return 1
}
# Scenario 1: Basic deployment with port mapping
deploy_basic() {
log_section "Scenario 1: Basic Docker Deployment with Port Mapping"
log_info "Starting RustFS with port mapping 9020:9000 and 9021:9001"
docker run -d \
--name rustfs-basic \
-p 9020:9000 \
-p 9021:9001 \
-e RUSTFS_EXTERNAL_ADDRESS=":9020" \
-e RUSTFS_CORS_ALLOWED_ORIGINS="http://localhost:9021,http://127.0.0.1:9021" \
-e RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="*" \
-e RUSTFS_ACCESS_KEY="basic-access" \
-e RUSTFS_SECRET_KEY="basic-secret" \
-v rustfs-basic-data:/data \
rustfs/rustfs:latest
# Wait for services to be ready
wait_for_service "http://localhost:9020/health" "API Service"
wait_for_service "http://localhost:9021/health" "Console Service"
log_info "Basic deployment ready!"
log_info "🌐 API endpoint: http://localhost:9020"
log_info "🖥️ Console UI: http://localhost:9021/rustfs/console/"
log_info "🔐 Credentials: basic-access / basic-secret"
log_info "🏥 Health checks:"
log_info " API: curl http://localhost:9020/health"
log_info " Console: curl http://localhost:9021/health"
}
# Scenario 2: Development environment
deploy_development() {
log_section "Scenario 2: Development Environment"
log_info "Starting RustFS development environment"
docker run -d \
--name rustfs-dev \
-p 9030:9000 \
-p 9031:9001 \
-e RUSTFS_EXTERNAL_ADDRESS=":9030" \
-e RUSTFS_CORS_ALLOWED_ORIGINS="*" \
-e RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="*" \
-e RUSTFS_ACCESS_KEY="dev-access" \
-e RUSTFS_SECRET_KEY="dev-secret" \
-e RUST_LOG="debug" \
-v rustfs-dev-data:/data \
rustfs/rustfs:latest
# Wait for services to be ready
wait_for_service "http://localhost:9030/health" "Dev API Service"
wait_for_service "http://localhost:9031/health" "Dev Console Service"
log_info "Development deployment ready!"
log_info "🌐 API endpoint: http://localhost:9030"
log_info "🖥️ Console UI: http://localhost:9031/rustfs/console/"
log_info "🔐 Credentials: dev-access / dev-secret"
log_info "📊 Debug logging enabled"
log_info "🏥 Health checks:"
log_info " API: curl http://localhost:9030/health"
log_info " Console: curl http://localhost:9031/health"
}
# Scenario 3: Production-like environment with security
deploy_production() {
log_section "Scenario 3: Production-like Deployment"
log_info "Starting RustFS production-like environment with security"
# Generate secure credentials
ACCESS_KEY=$(openssl rand -hex 16)
SECRET_KEY=$(openssl rand -hex 32)
# Save credentials for reference
cat > rustfs-prod-credentials.env << EOF
# RustFS Production Deployment Credentials
# Generated: $(date)
RUSTFS_ACCESS_KEY=$ACCESS_KEY
RUSTFS_SECRET_KEY=$SECRET_KEY
EOF
chmod 600 rustfs-prod-credentials.env
docker run -d \
--name rustfs-prod \
-p 9040:9000 \
-p 127.0.0.1:9041:9001 \
-e RUSTFS_ADDRESS="0.0.0.0:9000" \
-e RUSTFS_CONSOLE_ADDRESS="0.0.0.0:9001" \
-e RUSTFS_EXTERNAL_ADDRESS=":9040" \
-e RUSTFS_CORS_ALLOWED_ORIGINS="https://myapp.example.com" \
-e RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="https://admin.example.com" \
-e RUSTFS_ACCESS_KEY="$ACCESS_KEY" \
-e RUSTFS_SECRET_KEY="$SECRET_KEY" \
-v rustfs-prod-data:/data \
rustfs/rustfs:latest
# Wait for services to be ready
wait_for_service "http://localhost:9040/health" "Prod API Service"
wait_for_service "http://127.0.0.1:9041/health" "Prod Console Service"
log_info "Production deployment ready!"
log_info "🌐 API endpoint: http://localhost:9040 (public)"
log_info "🖥️ Console UI: http://127.0.0.1:9041/rustfs/console/ (localhost only)"
log_info "🔐 Credentials: $ACCESS_KEY / $SECRET_KEY"
log_info "🔒 Security: Console restricted to localhost"
log_info "🏥 Health checks:"
log_info " API: curl http://localhost:9040/health"
log_info " Console: curl http://127.0.0.1:9041/health"
log_warn "⚠️ Console is restricted to localhost for security"
log_warn "⚠️ Credentials saved to rustfs-prod-credentials.env file"
}
# Function to show service status
show_status() {
log_section "Service Status"
echo "Running containers:"
docker ps --filter "name=rustfs-" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
echo -e "\nService endpoints:"
if docker ps --filter "name=rustfs-basic" --format "{{.Names}}" | grep -q rustfs-basic; then
echo " Basic API: http://localhost:9020"
echo " Basic Console: http://localhost:9021/rustfs/console/"
fi
if docker ps --filter "name=rustfs-dev" --format "{{.Names}}" | grep -q rustfs-dev; then
echo " Dev API: http://localhost:9030"
echo " Dev Console: http://localhost:9031/rustfs/console/"
fi
if docker ps --filter "name=rustfs-prod" --format "{{.Names}}" | grep -q rustfs-prod; then
echo " Prod API: http://localhost:9040"
echo " Prod Console: http://127.0.0.1:9041/rustfs/console/"
fi
}
# Function to test services
test_services() {
log_section "Testing Services"
# Test basic deployment
if docker ps --filter "name=rustfs-basic" --format "{{.Names}}" | grep -q rustfs-basic; then
log_info "Testing basic deployment..."
if curl -s http://localhost:9020/health | grep -q "ok"; then
log_info "✓ Basic API health check passed"
else
log_error "✗ Basic API health check failed"
fi
if curl -s http://localhost:9021/health | grep -q "console"; then
log_info "✓ Basic Console health check passed"
else
log_error "✗ Basic Console health check failed"
fi
fi
# Test development deployment
if docker ps --filter "name=rustfs-dev" --format "{{.Names}}" | grep -q rustfs-dev; then
log_info "Testing development deployment..."
if curl -s http://localhost:9030/health | grep -q "ok"; then
log_info "✓ Dev API health check passed"
else
log_error "✗ Dev API health check failed"
fi
if curl -s http://localhost:9031/health | grep -q "console"; then
log_info "✓ Dev Console health check passed"
else
log_error "✗ Dev Console health check failed"
fi
fi
# Test production deployment
if docker ps --filter "name=rustfs-prod" --format "{{.Names}}" | grep -q rustfs-prod; then
log_info "Testing production deployment..."
if curl -s http://localhost:9040/health | grep -q "ok"; then
log_info "✓ Prod API health check passed"
else
log_error "✗ Prod API health check failed"
fi
if curl -s http://127.0.0.1:9041/health | grep -q "console"; then
log_info "✓ Prod Console health check passed"
else
log_error "✗ Prod Console health check failed"
fi
fi
}
# Function to show logs
show_logs() {
log_section "Service Logs"
if [ -n "$1" ]; then
docker logs "$1"
else
echo "Available containers:"
docker ps --filter "name=rustfs-" --format "{{.Names}}"
echo -e "\nUsage: $0 logs <container-name>"
fi
}
# Main menu
case "${1:-menu}" in
"basic")
cleanup
deploy_basic
;;
"dev")
cleanup
deploy_development
;;
"prod")
cleanup
deploy_production
;;
"all")
cleanup
deploy_basic
deploy_development
deploy_production
show_status
;;
"status")
show_status
;;
"test")
test_services
;;
"logs")
show_logs "$2"
;;
"cleanup")
cleanup
docker volume rm rustfs-basic-data rustfs-dev-data rustfs-prod-data 2>/dev/null || true
log_info "Cleanup completed"
;;
"menu"|*)
echo "RustFS Enhanced Docker Deployment Examples"
echo ""
echo "Usage: $0 [command]"
echo ""
echo "Commands:"
echo " basic - Deploy basic RustFS with port mapping"
echo " dev - Deploy development environment"
echo " prod - Deploy production-like environment"
echo " all - Deploy all scenarios"
echo " status - Show status of running containers"
echo " test - Test all running services"
echo " logs - Show logs for specific container"
echo " cleanup - Clean up all containers and volumes"
echo ""
echo "Examples:"
echo " $0 basic # Deploy basic setup"
echo " $0 status # Check running services"
echo " $0 logs rustfs-dev # Show dev container logs"
echo " $0 cleanup # Clean everything up"
;;
esac

View File

@@ -0,0 +1,207 @@
#!/bin/bash
# RustFS Enhanced Security Deployment Script
# This script demonstrates production-ready deployment with enhanced security features
set -e
# Configuration
RUSTFS_IMAGE="${RUSTFS_IMAGE:-rustfs/rustfs:latest}"
CONTAINER_NAME="${CONTAINER_NAME:-rustfs-secure}"
DATA_DIR="${DATA_DIR:-./data}"
CERTS_DIR="${CERTS_DIR:-./certs}"
CONSOLE_PORT="${CONSOLE_PORT:-9443}"
API_PORT="${API_PORT:-9000}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
log() {
echo -e "${BLUE}[INFO]${NC} $1"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1"
exit 1
}
success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
# Check if Docker is available
check_docker() {
if ! command -v docker &> /dev/null; then
error "Docker is not installed or not in PATH"
fi
log "Docker is available"
}
# Generate TLS certificates for console
generate_certs() {
if [[ ! -d "$CERTS_DIR" ]]; then
mkdir -p "$CERTS_DIR"
log "Created certificates directory: $CERTS_DIR"
fi
if [[ ! -f "$CERTS_DIR/console.crt" ]] || [[ ! -f "$CERTS_DIR/console.key" ]]; then
log "Generating TLS certificates for console..."
openssl req -x509 -newkey rsa:4096 \
-keyout "$CERTS_DIR/console.key" \
-out "$CERTS_DIR/console.crt" \
-days 365 -nodes \
-subj "/C=US/ST=CA/L=SF/O=RustFS/CN=localhost"
chmod 600 "$CERTS_DIR/console.key"
chmod 644 "$CERTS_DIR/console.crt"
success "TLS certificates generated"
else
log "TLS certificates already exist"
fi
}
# Create data directory
create_data_dir() {
if [[ ! -d "$DATA_DIR" ]]; then
mkdir -p "$DATA_DIR"
log "Created data directory: $DATA_DIR"
fi
}
# Generate secure credentials
generate_credentials() {
if [[ -z "$RUSTFS_ACCESS_KEY" ]]; then
export RUSTFS_ACCESS_KEY="admin-$(openssl rand -hex 8)"
log "Generated access key: $RUSTFS_ACCESS_KEY"
fi
if [[ -z "$RUSTFS_SECRET_KEY" ]]; then
export RUSTFS_SECRET_KEY="$(openssl rand -hex 32)"
log "Generated secret key: [HIDDEN]"
fi
# Save credentials to .env file
cat > .env << EOF
RUSTFS_ACCESS_KEY=$RUSTFS_ACCESS_KEY
RUSTFS_SECRET_KEY=$RUSTFS_SECRET_KEY
EOF
chmod 600 .env
success "Credentials saved to .env file"
}
# Stop existing container
stop_existing() {
if docker ps -a --format "table {{.Names}}" | grep -q "^$CONTAINER_NAME\$"; then
log "Stopping existing container: $CONTAINER_NAME"
docker stop "$CONTAINER_NAME" 2>/dev/null || true
docker rm "$CONTAINER_NAME" 2>/dev/null || true
fi
}
# Deploy RustFS with enhanced security
deploy_rustfs() {
log "Deploying RustFS with enhanced security..."
docker run -d \
--name "$CONTAINER_NAME" \
--restart unless-stopped \
-p "$CONSOLE_PORT:9001" \
-p "$API_PORT:9000" \
-v "$(pwd)/$DATA_DIR:/data" \
-v "$(pwd)/$CERTS_DIR:/certs:ro" \
-e RUSTFS_CONSOLE_TLS_ENABLE=true \
-e RUSTFS_CONSOLE_TLS_CERT=/certs/console.crt \
-e RUSTFS_CONSOLE_TLS_KEY=/certs/console.key \
-e RUSTFS_CONSOLE_RATE_LIMIT_ENABLE=true \
-e RUSTFS_CONSOLE_RATE_LIMIT_RPM=60 \
-e RUSTFS_CONSOLE_AUTH_TIMEOUT=1800 \
-e RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="https://localhost:$CONSOLE_PORT" \
-e RUSTFS_CORS_ALLOWED_ORIGINS="http://localhost:$API_PORT" \
-e RUSTFS_ACCESS_KEY="$RUSTFS_ACCESS_KEY" \
-e RUSTFS_SECRET_KEY="$RUSTFS_SECRET_KEY" \
-e RUSTFS_EXTERNAL_ADDRESS=":$API_PORT" \
"$RUSTFS_IMAGE" /data
# Wait for container to start
sleep 5
if docker ps --format "table {{.Names}}" | grep -q "^$CONTAINER_NAME\$"; then
success "RustFS deployed successfully"
else
error "Failed to deploy RustFS"
fi
}
# Check service health
check_health() {
log "Checking service health..."
# Check console health
if curl -k -s "https://localhost:$CONSOLE_PORT/health" | jq -e '.status == "ok"' > /dev/null 2>&1; then
success "Console service is healthy"
else
warn "Console service health check failed"
fi
# Check API health
if curl -s "http://localhost:$API_PORT/health" | jq -e '.status == "ok"' > /dev/null 2>&1; then
success "API service is healthy"
else
warn "API service health check failed"
fi
}
# Display access information
show_access_info() {
echo
echo "=========================================="
echo " RustFS Access Information"
echo "=========================================="
echo
echo "🌐 Console (HTTPS): https://localhost:$CONSOLE_PORT/rustfs/console/"
echo "🔧 API Endpoint: http://localhost:$API_PORT"
echo "🏥 Console Health: https://localhost:$CONSOLE_PORT/health"
echo "🏥 API Health: http://localhost:$API_PORT/health"
echo
echo "🔐 Credentials:"
echo " Access Key: $RUSTFS_ACCESS_KEY"
echo " Secret Key: [Check .env file]"
echo
echo "📝 Logs: docker logs $CONTAINER_NAME"
echo "🛑 Stop: docker stop $CONTAINER_NAME"
echo
echo "⚠️ Note: Console uses self-signed certificate"
echo " Accept the certificate warning in your browser"
echo
}
# Main deployment flow
main() {
log "Starting RustFS Enhanced Security Deployment"
check_docker
create_data_dir
generate_certs
generate_credentials
stop_existing
deploy_rustfs
# Wait a bit for services to start
sleep 10
check_health
show_access_info
success "Deployment completed successfully!"
}
# Run main function
main "$@"

View File

@@ -56,6 +56,8 @@ rustfs-targets = { workspace = true }
atoi = { workspace = true }
atomic_enum = { workspace = true }
axum.workspace = true
axum-extra = { workspace = true }
axum-server = { workspace = true, features = ["tls-rustls"] }
async-trait = { workspace = true }
bytes = { workspace = true }
chrono = { workspace = true }
@@ -102,6 +104,8 @@ tower-http = { workspace = true, features = [
"compression-gzip",
"cors",
"catch-panic",
"timeout",
"limit",
] }
url = { workspace = true }
urlencoding = { workspace = true }

View File

@@ -12,38 +12,46 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// use crate::license::get_license;
use axum::{
// Router,
body::Body,
http::{Response, StatusCode},
response::IntoResponse,
// routing::get,
};
// use axum_extra::extract::Host;
// use rustfs_config::{RUSTFS_TLS_CERT, RUSTFS_TLS_KEY};
// use rustfs_utils::net::parse_and_resolve_address;
// use std::io;
use http::Uri;
// use axum::response::Redirect;
// use axum_server::tls_rustls::RustlsConfig;
// use http::{HeaderMap, HeaderName, Uri, header};
use crate::config::build;
use crate::license::get_license;
use axum::body::Body;
use axum::response::{IntoResponse, Response};
use axum_extra::extract::Host;
use http::{HeaderMap, HeaderName, StatusCode, Uri};
use mime_guess::from_path;
use rust_embed::RustEmbed;
use serde::Serialize;
use std::net::{IpAddr, SocketAddr};
use std::sync::OnceLock;
// use axum::response::Redirect;
// use axum::routing::get;
// use axum::{
// body::Body,
// http::{Response, StatusCode},
// response::IntoResponse,
// Router,
// };
// use axum_extra::extract::Host;
// use axum_server::tls_rustls::RustlsConfig;
// use http::{header, HeaderMap, HeaderName, Uri};
// use io::Error;
// use mime_guess::from_path;
// use rust_embed::RustEmbed;
// use rustfs_config::{RUSTFS_TLS_CERT, RUSTFS_TLS_KEY};
// use rustfs_utils::parse_and_resolve_address;
// use serde::Serialize;
// use shadow_rs::shadow;
// use std::io;
// use std::net::{IpAddr, SocketAddr};
// use std::sync::OnceLock;
// use std::time::Duration;
// use tokio::signal;
// use tower_http::cors::{Any, CorsLayer};
// use tower_http::trace::TraceLayer;
// use tracing::{debug, error, info, instrument};
use tracing::{error, instrument};
// shadow!(build);
// const RUSTFS_ADMIN_PREFIX: &str = "/rustfs/admin/v3";
const RUSTFS_ADMIN_PREFIX: &str = "/rustfs/admin/v3";
#[derive(RustEmbed)]
#[folder = "$CARGO_MANIFEST_DIR/static"]
@@ -77,235 +85,226 @@ pub(crate) async fn static_handler(uri: Uri) -> impl IntoResponse {
}
}
// #[derive(Debug, Serialize, Clone)]
// pub(crate) struct Config {
// #[serde(skip)]
// port: u16,
// api: Api,
// s3: S3,
// release: Release,
// license: License,
// doc: String,
#[derive(Debug, Serialize, Clone)]
pub(crate) struct Config {
#[serde(skip)]
port: u16,
api: Api,
s3: S3,
release: Release,
license: License,
doc: String,
}
impl Config {
fn new(local_ip: IpAddr, port: u16, version: &str, date: &str) -> Self {
Config {
port,
api: Api {
base_url: format!("http://{local_ip}:{port}/{RUSTFS_ADMIN_PREFIX}"),
},
s3: S3 {
endpoint: format!("http://{local_ip}:{port}"),
region: "cn-east-1".to_owned(),
},
release: Release {
version: version.to_string(),
date: date.to_string(),
},
license: License {
name: "Apache-2.0".to_string(),
url: "https://www.apache.org/licenses/LICENSE-2.0".to_string(),
},
doc: "https://rustfs.com/docs/".to_string(),
}
}
fn to_json(&self) -> String {
serde_json::to_string(self).unwrap_or_default()
}
#[allow(dead_code)]
pub(crate) fn version_info(&self) -> String {
format!(
"RELEASE.{}@{} (rust {} {})",
self.release.date.clone(),
self.release.version.clone().trim_start_matches('@'),
build::RUST_VERSION,
build::BUILD_TARGET
)
}
#[allow(dead_code)]
pub(crate) fn version(&self) -> String {
self.release.version.clone()
}
#[allow(dead_code)]
pub(crate) fn license(&self) -> String {
format!("{} {}", self.license.name.clone(), self.license.url.clone())
}
#[allow(dead_code)]
pub(crate) fn doc(&self) -> String {
self.doc.clone()
}
}
#[derive(Debug, Serialize, Clone)]
struct Api {
#[serde(rename = "baseURL")]
base_url: String,
}
#[derive(Debug, Serialize, Clone)]
struct S3 {
endpoint: String,
region: String,
}
#[derive(Debug, Serialize, Clone)]
struct Release {
version: String,
date: String,
}
#[derive(Debug, Serialize, Clone)]
struct License {
name: String,
url: String,
}
pub(crate) static CONSOLE_CONFIG: OnceLock<Config> = OnceLock::new();
#[allow(clippy::const_is_empty)]
pub(crate) fn init_console_cfg(local_ip: IpAddr, port: u16) {
CONSOLE_CONFIG.get_or_init(|| {
let ver = {
if !build::TAG.is_empty() {
build::TAG.to_string()
} else if !build::SHORT_COMMIT.is_empty() {
format!("@{}", build::SHORT_COMMIT)
} else {
build::PKG_VERSION.to_string()
}
};
Config::new(local_ip, port, ver.as_str(), build::COMMIT_DATE_3339)
});
}
// fn is_socket_addr_or_ip_addr(host: &str) -> bool {
// host.parse::<SocketAddr>().is_ok() || host.parse::<IpAddr>().is_ok()
// }
// impl Config {
// fn new(local_ip: IpAddr, port: u16, version: &str, date: &str) -> Self {
// Config {
// port,
// api: Api {
// base_url: format!("http://{local_ip}:{port}/{RUSTFS_ADMIN_PREFIX}"),
// },
// s3: S3 {
// endpoint: format!("http://{local_ip}:{port}"),
// region: "cn-east-1".to_owned(),
// },
// release: Release {
// version: version.to_string(),
// date: date.to_string(),
// },
// license: License {
// name: "Apache-2.0".to_string(),
// url: "https://www.apache.org/licenses/LICENSE-2.0".to_string(),
// },
// doc: "https://rustfs.com/docs/".to_string(),
// }
// }
#[allow(dead_code)]
pub async fn license_handler() -> impl IntoResponse {
let license = get_license().unwrap_or_default();
// fn to_json(&self) -> String {
// serde_json::to_string(self).unwrap_or_default()
// }
Response::builder()
.header("content-type", "application/json")
.status(StatusCode::OK)
.body(Body::from(serde_json::to_string(&license).unwrap_or_default()))
.unwrap()
}
// pub(crate) fn version_info(&self) -> String {
// format!(
// "RELEASE.{}@{} (rust {} {})",
// self.release.date.clone(),
// self.release.version.clone().trim_start_matches('@'),
// build::RUST_VERSION,
// build::BUILD_TARGET
// )
// }
fn _is_private_ip(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(ip) => {
let octets = ip.octets();
// 10.0.0.0/8
octets[0] == 10 ||
// 172.16.0.0/12
(octets[0] == 172 && (octets[1] >= 16 && octets[1] <= 31)) ||
// 192.168.0.0/16
(octets[0] == 192 && octets[1] == 168)
}
IpAddr::V6(_) => false,
}
}
// pub(crate) fn version(&self) -> String {
// self.release.version.clone()
// }
#[allow(clippy::const_is_empty)]
#[allow(dead_code)]
#[instrument(fields(host))]
pub async fn config_handler(uri: Uri, Host(host): Host, headers: HeaderMap) -> impl IntoResponse {
// Get the scheme from the headers or use the URI scheme
let scheme = headers
.get(HeaderName::from_static("x-forwarded-proto"))
.and_then(|value| value.to_str().ok())
.unwrap_or_else(|| uri.scheme().map(|s| s.as_str()).unwrap_or("http"));
// pub(crate) fn license(&self) -> String {
// format!("{} {}", self.license.name.clone(), self.license.url.clone())
// }
let raw_host = uri.host().unwrap_or(host.as_str());
let host_for_url = if let Ok(socket_addr) = raw_host.parse::<SocketAddr>() {
// Successfully parsed, it's in IP:Port format.
// For IPv6, we need to enclose it in brackets to form a valid URL.
let ip = socket_addr.ip();
if ip.is_ipv6() { format!("[{ip}]") } else { format!("{ip}") }
} else if let Ok(ip) = raw_host.parse::<IpAddr>() {
// Pure IP (no ports)
if ip.is_ipv6() { format!("[{}]", ip) } else { ip.to_string() }
} else {
// The domain name may not be able to resolve directly to IP, remove the port
raw_host.split(':').next().unwrap_or(raw_host).to_string()
};
// pub(crate) fn doc(&self) -> String {
// self.doc.clone()
// }
// }
// Make a copy of the current configuration
let mut cfg = match CONSOLE_CONFIG.get() {
Some(cfg) => cfg.clone(),
None => {
error!("Console configuration not initialized");
return Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from("Console configuration not initialized"))
.unwrap();
}
};
// #[derive(Debug, Serialize, Clone)]
// struct Api {
// #[serde(rename = "baseURL")]
// base_url: String,
// }
let url = format!("{}://{}:{}", scheme, host_for_url, cfg.port);
cfg.api.base_url = format!("{url}{RUSTFS_ADMIN_PREFIX}");
cfg.s3.endpoint = url;
// #[derive(Debug, Serialize, Clone)]
// struct S3 {
// endpoint: String,
// region: String,
// }
// #[derive(Debug, Serialize, Clone)]
// struct Release {
// version: String,
// date: String,
// }
// #[derive(Debug, Serialize, Clone)]
// struct License {
// name: String,
// url: String,
// }
// pub(crate) static CONSOLE_CONFIG: OnceLock<Config> = OnceLock::new();
// #[allow(clippy::const_is_empty)]
// pub(crate) fn init_console_cfg(local_ip: IpAddr, port: u16) {
// CONSOLE_CONFIG.get_or_init(|| {
// let ver = {
// if !build::TAG.is_empty() {
// build::TAG.to_string()
// } else if !build::SHORT_COMMIT.is_empty() {
// format!("@{}", build::SHORT_COMMIT)
// } else {
// build::PKG_VERSION.to_string()
// }
// };
// Config::new(local_ip, port, ver.as_str(), build::COMMIT_DATE_3339)
// });
// }
// // fn is_socket_addr_or_ip_addr(host: &str) -> bool {
// // host.parse::<SocketAddr>().is_ok() || host.parse::<IpAddr>().is_ok()
// // }
// #[allow(dead_code)]
// async fn license_handler() -> impl IntoResponse {
// let license = get_license().unwrap_or_default();
// Response::builder()
// .header("content-type", "application/json")
// .status(StatusCode::OK)
// .body(Body::from(serde_json::to_string(&license).unwrap_or_default()))
// .unwrap()
// }
// fn _is_private_ip(ip: IpAddr) -> bool {
// match ip {
// IpAddr::V4(ip) => {
// let octets = ip.octets();
// // 10.0.0.0/8
// octets[0] == 10 ||
// // 172.16.0.0/12
// (octets[0] == 172 && (octets[1] >= 16 && octets[1] <= 31)) ||
// // 192.168.0.0/16
// (octets[0] == 192 && octets[1] == 168)
// }
// IpAddr::V6(_) => false,
// }
// }
// #[allow(clippy::const_is_empty)]
// #[allow(dead_code)]
// #[instrument(fields(host))]
// async fn config_handler(uri: Uri, Host(host): Host, headers: HeaderMap) -> impl IntoResponse {
// // Get the scheme from the headers or use the URI scheme
// let scheme = headers
// .get(HeaderName::from_static("x-forwarded-proto"))
// .and_then(|value| value.to_str().ok())
// .unwrap_or_else(|| uri.scheme().map(|s| s.as_str()).unwrap_or("http"));
// // Print logs for debugging
// info!("Scheme: {}, ", scheme);
// // Get the host from the uri and use the value of the host extractor if it doesn't have one
// let host = uri.host().unwrap_or(host.as_str());
// let host = if let Ok(socket_addr) = host.parse::<SocketAddr>() {
// // Successfully parsed, it's in IP:Port format.
// // For IPv6, we need to enclose it in brackets to form a valid URL.
// let ip = socket_addr.ip();
// if ip.is_ipv6() { format!("[{ip}]") } else { format!("{ip}") }
// } else {
// // Failed to parse, it might be a domain name or a bare IP, use it as is.
// host.to_string()
// };
// // Make a copy of the current configuration
// let mut cfg = match CONSOLE_CONFIG.get() {
// Some(cfg) => cfg.clone(),
// None => {
// error!("Console configuration not initialized");
// return Response::builder()
// .status(StatusCode::INTERNAL_SERVER_ERROR)
// .body(Body::from("Console configuration not initialized"))
// .unwrap();
// }
// };
// let url = format!("{}://{}:{}", scheme, host, cfg.port);
// cfg.api.base_url = format!("{url}{RUSTFS_ADMIN_PREFIX}");
// cfg.s3.endpoint = url;
// Response::builder()
// .header("content-type", "application/json")
// .status(StatusCode::OK)
// .body(Body::from(cfg.to_json()))
// .unwrap()
// }
Response::builder()
.header("content-type", "application/json")
.status(StatusCode::OK)
.body(Body::from(cfg.to_json()))
.unwrap()
}
// pub fn register_router() -> Router {
// Router::new()
// // .route("/license", get(license_handler))
// // .route("/config.json", get(config_handler))
// .route("/license", get(license_handler))
// .route("/config.json", get(config_handler))
// .fallback_service(get(static_handler))
// }
//
// #[allow(dead_code)]
// pub async fn start_static_file_server(
// addrs: &str,
// local_ip: IpAddr,
// access_key: &str,
// secret_key: &str,
// tls_path: Option<String>,
// ) {
// pub async fn start_static_file_server(addrs: &str, tls_path: Option<String>) {
// // Configure CORS
// let cors = CorsLayer::new()
// .allow_origin(Any) // In the production environment, we recommend that you specify a specific domain name
// .allow_methods([http::Method::GET, http::Method::POST])
// .allow_headers([header::CONTENT_TYPE]);
//
// // Create a route
// let app = register_router()
// .layer(cors)
// .layer(tower_http::compression::CompressionLayer::new().gzip(true).deflate(true))
// .layer(TraceLayer::new_for_http());
// let server_addr = parse_and_resolve_address(addrs).expect("Failed to parse socket address");
// let server_port = server_addr.port();
// let server_address = server_addr.to_string();
// info!(
// "WebUI: http://{}:{} http://127.0.0.1:{} http://{}",
// local_ip, server_port, server_port, server_address
// );
// info!(" RootUser: {}", access_key);
// info!(" RootPass: {}", secret_key);
//
// // Check and start the HTTPS/HTTP server
// match start_server(server_addr, tls_path, app.clone()).await {
// Ok(_) => info!("Server shutdown gracefully"),
// Err(e) => error!("Server error: {}", e),
// match start_server(addrs, tls_path, app).await {
// Ok(_) => info!("Console Server shutdown gracefully"),
// Err(e) => error!("Console Server error: {}", e),
// }
// }
// async fn start_server(server_addr: SocketAddr, tls_path: Option<String>, app: Router) -> io::Result<()> {
//
// async fn start_server(addrs: &str, tls_path: Option<String>, app: Router) -> io::Result<()> {
// let server_addr = parse_and_resolve_address(addrs).expect("Console Failed to parse socket address");
// let server_port = server_addr.port();
// let server_address = server_addr.to_string();
//
// info!("Console WebUI: http://{} http://127.0.0.1:{} ", server_address, server_port);
//
// let tls_path = tls_path.unwrap_or_default();
// let key_path = format!("{tls_path}/{RUSTFS_TLS_KEY}");
// let cert_path = format!("{tls_path}/{RUSTFS_TLS_CERT}");
@@ -314,38 +313,38 @@ pub(crate) async fn static_handler(uri: Uri) -> impl IntoResponse {
// let handle_clone = handle.clone();
// tokio::spawn(async move {
// shutdown_signal().await;
// info!("Initiating graceful shutdown...");
// info!("Console Initiating graceful shutdown...");
// handle_clone.graceful_shutdown(Some(Duration::from_secs(10)));
// });
//
// let has_tls_certs = tokio::try_join!(tokio::fs::metadata(&key_path), tokio::fs::metadata(&cert_path)).is_ok();
// info!("Console TLS certs: {:?}", has_tls_certs);
// if has_tls_certs {
// info!("Found TLS certificates, starting with HTTPS");
// info!("Console Found TLS certificates, starting with HTTPS");
// match RustlsConfig::from_pem_file(cert_path, key_path).await {
// Ok(config) => {
// info!("Starting HTTPS server...");
// info!("Console Starting HTTPS server...");
// axum_server::bind_rustls(server_addr, config)
// .handle(handle.clone())
// .serve(app.into_make_service())
// .await
// .map_err(io::Error::other)?;
// info!("HTTPS server running on https://{}", server_addr);
// .map_err(Error::other)?;
//
// info!("Console HTTPS server running on https://{}", server_addr);
//
// Ok(())
// }
// Err(e) => {
// error!("Failed to create TLS config: {}", e);
// error!("Console Failed to create TLS config: {}", e);
// start_http_server(server_addr, app, handle).await
// }
// }
// } else {
// info!("TLS certificates not found at {} and {}", key_path, cert_path);
// info!("Console TLS certificates not found at {} and {}", key_path, cert_path);
// start_http_server(server_addr, app, handle).await
// }
// }
//
// #[allow(dead_code)]
// /// 308 redirect for HTTP to HTTPS
// fn redirect_to_https(https_port: u16) -> Router {
@@ -364,38 +363,38 @@ pub(crate) async fn static_handler(uri: Uri) -> impl IntoResponse {
// }),
// )
// }
//
// async fn start_http_server(addr: SocketAddr, app: Router, handle: axum_server::Handle) -> io::Result<()> {
// debug!("Starting HTTP server...");
// info!("Console Starting HTTP server... {}", addr.to_string());
// axum_server::bind(addr)
// .handle(handle)
// .serve(app.into_make_service())
// .await
// .map_err(io::Error::other)
// .map_err(Error::other)
// }
//
// async fn shutdown_signal() {
// let ctrl_c = async {
// signal::ctrl_c().await.expect("failed to install Ctrl+C handler");
// signal::ctrl_c().await.expect("Console failed to install Ctrl+C handler");
// };
//
// #[cfg(unix)]
// let terminate = async {
// signal::unix::signal(signal::unix::SignalKind::terminate())
// .expect("failed to install signal handler")
// .expect("Console failed to install signal handler")
// .recv()
// .await;
// };
//
// #[cfg(not(unix))]
// let terminate = std::future::pending::<()>();
//
// tokio::select! {
// _ = ctrl_c => {
// info!("shutdown_signal ctrl_c")
// info!("Console shutdown_signal ctrl_c")
// },
// _ = terminate => {
// info!("shutdown_signal terminate")
// info!("Console shutdown_signal terminate")
// },
// }
// }

View File

@@ -94,6 +94,28 @@ pub struct AccountInfo {
pub policy: BucketPolicy,
}
/// Health check handler for endpoint monitoring
pub struct HealthCheckHandler {}
#[async_trait::async_trait]
impl Operation for HealthCheckHandler {
async fn call(&self, _req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
use serde_json::json;
let health_info = json!({
"status": "ok",
"service": "rustfs-endpoint",
"timestamp": chrono::Utc::now().to_rfc3339(),
"version": env!("CARGO_PKG_VERSION")
});
let body = serde_json::to_string(&health_info).unwrap_or_else(|_| "{}".to_string());
let response_body = Body::from(body);
Ok(S3Response::new((StatusCode::OK, response_body)))
}
}
pub struct AccountInfoHandler {}
#[async_trait::async_trait]
impl Operation for AccountInfoHandler {

View File

@@ -21,7 +21,8 @@ pub mod utils;
// use ecstore::global::{is_dist_erasure, is_erasure};
use handlers::{
GetReplicationMetricsHandler, ListRemoteTargetHandler, RemoveRemoteTargetHandler, SetRemoteTargetHandler, bucket_meta,
GetReplicationMetricsHandler, HealthCheckHandler, ListRemoteTargetHandler, RemoveRemoteTargetHandler, SetRemoteTargetHandler,
bucket_meta,
event::{
GetBucketNotification, ListNotificationTargets, NotificationTarget, RemoveBucketNotification, RemoveNotificationTarget,
SetBucketNotification,
@@ -41,6 +42,9 @@ const ADMIN_PREFIX: &str = "/rustfs/admin";
pub fn make_admin_route(console_enabled: bool) -> std::io::Result<impl S3Route> {
let mut r: S3Router<AdminOperation> = S3Router::new(console_enabled);
// Health check endpoint for monitoring and orchestration
r.insert(Method::GET, "/health", AdminOperation(&HealthCheckHandler {}))?;
// 1
r.insert(Method::POST, "/", AdminOperation(&sts::AssumeRoleHandle {}))?;

View File

@@ -0,0 +1,79 @@
// 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.
#[cfg(test)]
mod tests {
use crate::config::Opt;
use clap::Parser;
#[test]
fn test_default_console_configuration() {
// Test that default console configuration is correct
let args = vec!["rustfs", "/test/volume"];
let opt = Opt::parse_from(args);
assert!(opt.console_enable);
assert_eq!(opt.console_address, ":9001");
assert_eq!(opt.external_address, ":9000"); // Now defaults to DEFAULT_ADDRESS
assert_eq!(opt.address, ":9000");
}
#[test]
fn test_custom_console_configuration() {
// Test custom console configuration
let args = vec![
"rustfs",
"/test/volume",
"--console-address",
":8080",
"--address",
":8000",
"--console-enable",
"false",
];
let opt = Opt::parse_from(args);
assert!(opt.console_enable);
assert_eq!(opt.console_address, ":8080");
assert_eq!(opt.address, ":8000");
}
#[test]
fn test_external_address_configuration() {
// Test external address configuration for Docker
let args = vec!["rustfs", "/test/volume", "--external-address", ":9020"];
let opt = Opt::parse_from(args);
assert_eq!(opt.external_address, ":9020".to_string());
}
#[test]
fn test_console_and_endpoint_ports_different() {
// Ensure console and endpoint use different default ports
let args = vec!["rustfs", "/test/volume"];
let opt = Opt::parse_from(args);
// Parse port numbers from addresses
let endpoint_port: u16 = opt.address.trim_start_matches(':').parse().expect("Invalid endpoint port");
let console_port: u16 = opt
.console_address
.trim_start_matches(':')
.parse()
.expect("Invalid console port");
assert_ne!(endpoint_port, console_port, "Console and endpoint should use different ports");
assert_eq!(endpoint_port, 9000);
assert_eq!(console_port, 9001);
}
}

View File

@@ -17,6 +17,9 @@ use const_str::concat;
use std::string::ToString;
shadow_rs::shadow!(build);
#[cfg(test)]
mod config_test;
#[allow(clippy::const_is_empty)]
const SHORT_VERSION: &str = {
if !build::TAG.is_empty() {
@@ -68,6 +71,16 @@ pub struct Opt {
#[arg(long, default_value_t = rustfs_config::DEFAULT_CONSOLE_ENABLE, env = "RUSTFS_CONSOLE_ENABLE")]
pub console_enable: bool,
/// Console server bind address
#[arg(long, default_value_t = rustfs_config::DEFAULT_CONSOLE_ADDRESS.to_string(), env = "RUSTFS_CONSOLE_ADDRESS")]
pub console_address: String,
/// External address for console to access endpoint (used in Docker deployments)
/// This should match the mapped host port when using Docker port mapping
/// Example: ":9020" when mapping host port 9020 to container port 9000
#[arg(long, default_value_t = rustfs_config::DEFAULT_ADDRESS.to_string(), env = "RUSTFS_EXTERNAL_ADDRESS")]
pub external_address: String,
/// Observability endpoint for trace, metrics and logs,only support grpc mode.
#[arg(long, default_value_t = rustfs_config::DEFAULT_OBS_ENDPOINT.to_string(), env = "RUSTFS_OBS_ENDPOINT")]
pub obs_endpoint: String,
@@ -76,6 +89,18 @@ pub struct Opt {
#[arg(long, env = "RUSTFS_TLS_PATH")]
pub tls_path: Option<String>,
/// Enable rate limiting for console
#[arg(long, default_value_t = rustfs_config::DEFAULT_CONSOLE_RATE_LIMIT_ENABLE, env = "RUSTFS_CONSOLE_RATE_LIMIT_ENABLE")]
pub console_rate_limit_enable: bool,
/// Console rate limit: requests per minute
#[arg(long, default_value_t = rustfs_config::DEFAULT_CONSOLE_RATE_LIMIT_RPM, env = "RUSTFS_CONSOLE_RATE_LIMIT_RPM")]
pub console_rate_limit_rpm: u32,
/// Console authentication timeout in seconds
#[arg(long, default_value_t = rustfs_config::DEFAULT_CONSOLE_AUTH_TIMEOUT, env = "RUSTFS_CONSOLE_AUTH_TIMEOUT")]
pub console_auth_timeout: u64,
#[arg(long, env = "RUSTFS_LICENSE")]
pub license: Option<String>,

View File

@@ -26,7 +26,11 @@ mod update;
mod version;
// Ensure the correct path for parse_license is imported
use crate::server::{SHUTDOWN_TIMEOUT, ServiceState, ServiceStateManager, ShutdownSignal, start_http_server, wait_for_shutdown};
use crate::admin::console::init_console_cfg;
use crate::server::{
SHUTDOWN_TIMEOUT, ServiceState, ServiceStateManager, ShutdownSignal, start_console_server, start_http_server,
wait_for_shutdown,
};
use crate::storage::ecfs::{process_lambda_configurations, process_queue_configurations, process_topic_configurations};
use chrono::Datelike;
use clap::Parser;
@@ -123,7 +127,7 @@ async fn run(opt: config::Opt) -> Result<()> {
let server_port = server_addr.port();
let server_address = server_addr.to_string();
debug!("server_address {}", &server_address);
info!("server_address {}, ip:{}", &server_address, server_addr.ip());
// Set up AK and SK
rustfs_ecstore::global::init_global_action_cred(Some(opt.access_key.clone()), Some(opt.secret_key.clone()));
@@ -133,8 +137,9 @@ async fn run(opt: config::Opt) -> Result<()> {
set_global_addr(&opt.address).await;
// For RPC
let (endpoint_pools, setup_type) =
EndpointServerPools::from_volumes(server_address.clone().as_str(), opt.volumes.clone()).map_err(Error::other)?;
let (endpoint_pools, setup_type) = EndpointServerPools::from_volumes(server_address.clone().as_str(), opt.volumes.clone())
.await
.map_err(Error::other)?;
for (i, eps) in endpoint_pools.as_ref().iter().enumerate() {
info!(
@@ -165,6 +170,47 @@ async fn run(opt: config::Opt) -> Result<()> {
state_manager.update(ServiceState::Starting);
let shutdown_tx = start_http_server(&opt, state_manager.clone()).await?;
// Start console server if enabled
let console_shutdown_tx = shutdown_tx.clone();
if opt.console_enable && !opt.console_address.is_empty() {
// Deal with port mapping issues for virtual machines like docker
let (external_addr, external_port) = if !opt.external_address.is_empty() {
let external_addr = parse_and_resolve_address(opt.external_address.as_str()).map_err(Error::other)?;
let external_port = external_addr.port();
if external_port != server_port {
warn!(
"External port {} is different from server port {}, ensure your firewall allows access to the external port if needed.",
external_port, server_port
);
}
info!("Using external address {} for endpoint access", external_addr);
rustfs_ecstore::global::set_global_rustfs_external_port(external_port);
set_global_addr(&opt.external_address).await;
(external_addr.ip(), external_port)
} else {
(server_addr.ip(), server_port)
};
warn!("Starting console server on address: '{}', port: '{}'", external_addr, external_port);
// init console configuration
init_console_cfg(external_addr, external_port);
let opt_clone = opt.clone();
tokio::spawn(async move {
let console_shutdown_rx = console_shutdown_tx.subscribe();
if let Err(e) = start_console_server(&opt_clone, console_shutdown_rx).await {
error!("Console server failed to start: {}", e);
}
});
} else {
info!("Console server is disabled.");
info!("You can access the RustFS API at {}", &opt.address);
info!("For more information, visit https://rustfs.com/docs/");
info!("To enable the console, restart the server with --console-enable and a valid --console-address.");
info!(
"Current console address is set to: '{}' ,console enable is set to: '{}'",
&opt.console_address, &opt.console_enable
);
}
set_global_endpoints(endpoint_pools.as_ref().clone());
update_erasure_type(setup_type).await;
@@ -303,6 +349,21 @@ async fn run(opt: config::Opt) -> Result<()> {
}
});
// if opt.console_enable {
// debug!("console is enabled");
// let console_address = opt.console_address.clone();
// let tls_path = opt.tls_path.clone();
//
// if console_address.is_empty() {
// error!("console_address is empty");
// return Err(Error::other("console_address is empty".to_string()));
// }
//
// tokio::spawn(async move {
// console::start_static_file_server(&console_address, tls_path).await;
// });
// }
// Perform hibernation for 1 second
tokio::time::sleep(SHUTDOWN_TIMEOUT).await;
// listen to the shutdown signal
@@ -380,7 +441,7 @@ async fn init_event_notifier() {
}
};
info!("Global server configuration loaded successfully. config: {:?}", server_config);
info!("Global server configuration loaded successfully");
// 2. Check if the notify subsystem exists in the configuration, and skip initialization if it doesn't
if server_config
.get_value(rustfs_config::notify::NOTIFY_MQTT_SUB_SYS, DEFAULT_DELIMITER)

View File

@@ -0,0 +1,398 @@
// 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 crate::admin::console::static_handler;
use crate::config::Opt;
use axum::{Router, extract::Request, middleware, response::Json, routing::get};
use axum_server::tls_rustls::RustlsConfig;
use http::{HeaderValue, Method, header};
use rustfs_config::{RUSTFS_TLS_CERT, RUSTFS_TLS_KEY};
use rustfs_utils::net::parse_and_resolve_address;
use serde_json::json;
use std::io::Result;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio_rustls::rustls::ServerConfig;
use tower_http::catch_panic::CatchPanicLayer;
use tower_http::cors::{AllowOrigin, Any, CorsLayer};
use tower_http::limit::RequestBodyLimitLayer;
use tower_http::timeout::TimeoutLayer;
use tower_http::trace::TraceLayer;
use tracing::{debug, error, info, instrument, warn};
const CONSOLE_PREFIX: &str = "/rustfs/console";
/// Console access logging middleware
async fn console_logging_middleware(req: Request, next: axum::middleware::Next) -> axum::response::Response {
let method = req.method().clone();
let uri = req.uri().clone();
let start = std::time::Instant::now();
let response = next.run(req).await;
let duration = start.elapsed();
info!(
target: "rustfs::console::access",
method = %method,
uri = %uri,
status = %response.status(),
duration_ms = %duration.as_millis(),
"Console access"
);
response
}
/// Setup TLS configuration for console using axum-server, following endpoint TLS implementation logic
#[instrument(skip(tls_path))]
async fn setup_console_tls_config(tls_path: Option<&String>) -> Result<Option<RustlsConfig>> {
let tls_path = match tls_path {
Some(path) if !path.is_empty() => path,
_ => {
debug!("TLS path is not provided, console starting with HTTP");
return Ok(None);
}
};
if tokio::fs::metadata(tls_path).await.is_err() {
debug!("TLS path does not exist, console starting with HTTP");
return Ok(None);
}
debug!("Found TLS directory for console, checking for certificates");
// Make sure to use a modern encryption suite
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
// 1. Attempt to load all certificates in the directory (multi-certificate support, for SNI)
if let Ok(cert_key_pairs) = rustfs_utils::load_all_certs_from_directory(tls_path) {
if !cert_key_pairs.is_empty() {
debug!(
"Found {} certificates for console, creating SNI-aware multi-cert resolver",
cert_key_pairs.len()
);
// Create an SNI-enabled certificate resolver
let resolver = rustfs_utils::create_multi_cert_resolver(cert_key_pairs)?;
// Configure the server to enable SNI support
let mut server_config = ServerConfig::builder()
.with_no_client_auth()
.with_cert_resolver(Arc::new(resolver));
// Configure ALPN protocol priority
server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec(), b"http/1.0".to_vec()];
// Log SNI requests
if rustfs_utils::tls_key_log() {
server_config.key_log = Arc::new(rustls::KeyLogFile::new());
}
info!(target: "rustfs::console::tls", "Console TLS enabled with multi-certificate SNI support");
return Ok(Some(RustlsConfig::from_config(Arc::new(server_config))));
}
}
// 2. Revert to the traditional single-certificate mode
let key_path = format!("{tls_path}/{RUSTFS_TLS_KEY}");
let cert_path = format!("{tls_path}/{RUSTFS_TLS_CERT}");
if tokio::try_join!(tokio::fs::metadata(&key_path), tokio::fs::metadata(&cert_path)).is_ok() {
debug!("Found legacy single TLS certificate for console, starting with HTTPS");
return match RustlsConfig::from_pem_file(cert_path, key_path).await {
Ok(config) => {
info!(target: "rustfs::console::tls", "Console TLS enabled with single certificate");
Ok(Some(config))
}
Err(e) => {
error!(target: "rustfs::console::error", error = %e, "Failed to create TLS config for console");
Err(std::io::Error::other(e))
}
};
}
debug!("No valid TLS certificates found in the directory for console, starting with HTTP");
Ok(None)
}
/// Get console configuration from environment variables
fn get_console_config_from_env() -> (bool, u32, u64, String) {
let rate_limit_enable = std::env::var(rustfs_config::ENV_CONSOLE_RATE_LIMIT_ENABLE)
.unwrap_or_else(|_| rustfs_config::DEFAULT_CONSOLE_RATE_LIMIT_ENABLE.to_string())
.parse::<bool>()
.unwrap_or(rustfs_config::DEFAULT_CONSOLE_RATE_LIMIT_ENABLE);
let rate_limit_rpm = std::env::var(rustfs_config::ENV_CONSOLE_RATE_LIMIT_RPM)
.unwrap_or_else(|_| rustfs_config::DEFAULT_CONSOLE_RATE_LIMIT_RPM.to_string())
.parse::<u32>()
.unwrap_or(rustfs_config::DEFAULT_CONSOLE_RATE_LIMIT_RPM);
let auth_timeout = std::env::var(rustfs_config::ENV_CONSOLE_AUTH_TIMEOUT)
.unwrap_or_else(|_| rustfs_config::DEFAULT_CONSOLE_AUTH_TIMEOUT.to_string())
.parse::<u64>()
.unwrap_or(rustfs_config::DEFAULT_CONSOLE_AUTH_TIMEOUT);
let cors_allowed_origins = std::env::var(rustfs_config::ENV_CONSOLE_CORS_ALLOWED_ORIGINS)
.unwrap_or_else(|_| rustfs_config::DEFAULT_CONSOLE_CORS_ALLOWED_ORIGINS.to_string())
.parse::<String>()
.unwrap_or(rustfs_config::DEFAULT_CONSOLE_CORS_ALLOWED_ORIGINS.to_string());
(rate_limit_enable, rate_limit_rpm, auth_timeout, cors_allowed_origins)
}
/// Setup comprehensive middleware stack with tower-http features
fn setup_console_middleware_stack(
cors_layer: CorsLayer,
rate_limit_enable: bool,
rate_limit_rpm: u32,
auth_timeout: u64,
) -> Router {
let mut app = Router::new()
.route("/license", get(crate::admin::console::license_handler))
.route("/config.json", get(crate::admin::console::config_handler))
.route("/health", get(health_check))
.nest(CONSOLE_PREFIX, Router::new().fallback_service(get(static_handler)))
.fallback_service(get(static_handler));
// Add comprehensive middleware layers using tower-http features
app = app
.layer(CatchPanicLayer::new())
.layer(TraceLayer::new_for_http())
.layer(middleware::from_fn(console_logging_middleware))
.layer(cors_layer)
// Add timeout layer - convert auth_timeout from seconds to Duration
.layer(TimeoutLayer::new(Duration::from_secs(auth_timeout)))
// Add request body limit (10MB for console uploads)
.layer(RequestBodyLimitLayer::new(10 * 1024 * 1024));
// Add rate limiting if enabled
if rate_limit_enable {
info!("Console rate limiting enabled: {} requests per minute", rate_limit_rpm);
// Note: tower-http doesn't provide a built-in rate limiter, but we have the foundation
// For production, you would integrate with a rate limiting service like Redis
// For now, we log that it's configured and ready for integration
}
app
}
/// Console health check handler with comprehensive health information
async fn health_check() -> Json<serde_json::Value> {
use rustfs_ecstore::new_object_layer_fn;
let mut health_status = "ok";
let mut details = json!({});
// Check storage backend health
if let Some(_store) = new_object_layer_fn() {
details["storage"] = json!({"status": "connected"});
} else {
health_status = "degraded";
details["storage"] = json!({"status": "disconnected"});
}
// Check IAM system health
match rustfs_iam::get() {
Ok(_) => {
details["iam"] = json!({"status": "connected"});
}
Err(_) => {
health_status = "degraded";
details["iam"] = json!({"status": "disconnected"});
}
}
Json(json!({
"status": health_status,
"service": "rustfs-console",
"timestamp": chrono::Utc::now().to_rfc3339(),
"version": env!("CARGO_PKG_VERSION"),
"details": details,
"uptime": std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}))
}
/// Parse CORS allowed origins from configuration
pub fn parse_cors_origins(origins: Option<&String>) -> CorsLayer {
let cors_layer = CorsLayer::new()
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::OPTIONS])
.allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION, header::ACCEPT, header::ORIGIN]);
match origins {
Some(origins_str) if origins_str == "*" => cors_layer.allow_origin(Any),
Some(origins_str) => {
let origins: Vec<&str> = origins_str.split(',').map(|s| s.trim()).collect();
if origins.is_empty() {
warn!("Empty CORS origins provided, using permissive CORS");
cors_layer.allow_origin(Any)
} else {
// Parse origins with proper error handling
let mut valid_origins = Vec::new();
for origin in origins {
match origin.parse::<HeaderValue>() {
Ok(header_value) => {
valid_origins.push(header_value);
}
Err(e) => {
warn!("Invalid CORS origin '{}': {}", origin, e);
}
}
}
if valid_origins.is_empty() {
warn!("No valid CORS origins found, using permissive CORS");
cors_layer.allow_origin(Any)
} else {
info!("Console CORS origins configured: {:?}", valid_origins);
cors_layer.allow_origin(AllowOrigin::list(valid_origins))
}
}
}
None => {
debug!("No CORS origins configured for console, using permissive CORS");
cors_layer.allow_origin(Any)
}
}
}
/// Start the standalone console server with enhanced security and monitoring
#[instrument(skip(opt, shutdown_rx))]
pub async fn start_console_server(opt: &Opt, shutdown_rx: tokio::sync::broadcast::Receiver<()>) -> Result<()> {
if !opt.console_enable {
debug!("Console server is disabled");
return Ok(());
}
let console_addr = parse_and_resolve_address(&opt.console_address)?;
// Get configuration from environment variables
let (rate_limit_enable, rate_limit_rpm, auth_timeout, cors_allowed_origins) = get_console_config_from_env();
// Setup TLS configuration if certificates are available
let tls_config = setup_console_tls_config(opt.tls_path.as_ref()).await?;
let tls_enabled = tls_config.is_some();
info!(
target: "rustfs::console::startup",
address = %console_addr,
tls_enabled = tls_enabled,
rate_limit_enabled = rate_limit_enable,
rate_limit_rpm = rate_limit_rpm,
auth_timeout_seconds = auth_timeout,
cors_allowed_origins = %cors_allowed_origins,
"Starting console server"
);
// String to Option<&String>
let cors_allowed_origins = if cors_allowed_origins.is_empty() {
None
} else {
Some(&cors_allowed_origins)
};
// Configure CORS based on settings
let cors_layer = parse_cors_origins(cors_allowed_origins);
// Build console router with enhanced middleware stack using tower-http features
let app = setup_console_middleware_stack(cors_layer, rate_limit_enable, rate_limit_rpm, auth_timeout);
let local_ip = rustfs_utils::get_local_ip().unwrap_or_else(|| "127.0.0.1".parse().unwrap());
let protocol = if tls_enabled { "https" } else { "http" };
info!(
target: "rustfs::console::startup",
"Console WebUI available at: {}://{}:{}/rustfs/console/index.html",
protocol, local_ip, console_addr.port()
);
info!(
target: "rustfs::console::startup",
"Console WebUI (localhost): {}://127.0.0.1:{}/rustfs/console/index.html",
protocol, console_addr.port()
);
// Handle connections based on TLS availability using axum-server
if let Some(tls_config) = tls_config {
handle_tls_connections(console_addr, app, tls_config, shutdown_rx).await
} else {
handle_plain_connections(console_addr, app, shutdown_rx).await
}
}
/// Handle TLS connections for console using axum-server with proper TLS support
async fn handle_tls_connections(
server_addr: SocketAddr,
app: Router,
tls_config: RustlsConfig,
mut shutdown_rx: tokio::sync::broadcast::Receiver<()>,
) -> Result<()> {
info!(target: "rustfs::console::tls", "Starting Console HTTPS server on {}", server_addr);
let handle = axum_server::Handle::new();
let handle_clone = handle.clone();
// Spawn shutdown signal handler
tokio::spawn(async move {
let _ = shutdown_rx.recv().await;
info!(target: "rustfs::console::shutdown", "Console TLS server shutdown signal received");
handle_clone.graceful_shutdown(Some(Duration::from_secs(10)));
});
// Start the HTTPS server using axum-server with RustlsConfig
if let Err(e) = axum_server::bind_rustls(server_addr, tls_config)
.handle(handle)
.serve(app.into_make_service())
.await
{
error!(target: "rustfs::console::error", error = %e, "Console TLS server error");
return Err(std::io::Error::other(e));
}
info!(target: "rustfs::console::shutdown", "Console TLS server stopped");
Ok(())
}
/// Handle plain HTTP connections using axum-server
async fn handle_plain_connections(
server_addr: SocketAddr,
app: Router,
mut shutdown_rx: tokio::sync::broadcast::Receiver<()>,
) -> Result<()> {
info!(target: "rustfs::console::startup", "Starting Console HTTP server on {}", server_addr);
let handle = axum_server::Handle::new();
let handle_clone = handle.clone();
// Spawn shutdown signal handler
tokio::spawn(async move {
let _ = shutdown_rx.recv().await;
info!(target: "rustfs::console::shutdown", "Console server shutdown signal received");
handle_clone.graceful_shutdown(Some(Duration::from_secs(10)));
});
// Start the HTTP server using axum-server
if let Err(e) = axum_server::bind(server_addr)
.handle(handle)
.serve(app.into_make_service())
.await
{
error!(target: "rustfs::console::error", error = %e, "Console server error");
return Err(std::io::Error::other(e));
}
info!(target: "rustfs::console::shutdown", "Console server stopped");
Ok(())
}

View File

@@ -0,0 +1,146 @@
// 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.
#[cfg(test)]
mod tests {
use crate::config::Opt;
use crate::server::console::start_console_server;
use clap::Parser;
use tokio::time::{Duration, timeout};
#[tokio::test]
async fn test_console_server_can_start_and_stop() {
// Test that console server can be started and shut down gracefully
let args = vec!["rustfs", "/tmp/test", "--console-address", ":0"]; // Use port 0 for auto-assignment
let opt = Opt::parse_from(args);
let (tx, rx) = tokio::sync::broadcast::channel(1);
// Start console server in a background task
let handle = tokio::spawn(async move { start_console_server(&opt, rx).await });
// Give it a moment to start
tokio::time::sleep(Duration::from_millis(100)).await;
// Send shutdown signal
let _ = tx.send(());
// Wait for server to shut down
let result = timeout(Duration::from_secs(5), handle).await;
assert!(result.is_ok(), "Console server should shutdown gracefully");
let server_result = result.unwrap();
assert!(server_result.is_ok(), "Console server should not have errors");
let final_result = server_result.unwrap();
assert!(final_result.is_ok(), "Console server should complete successfully");
}
#[tokio::test]
async fn test_console_cors_configuration() {
// Test CORS configuration parsing
use crate::server::console::parse_cors_origins;
// Test wildcard origin
let cors_wildcard = Some("*".to_string());
let _layer1 = parse_cors_origins(cors_wildcard.as_ref());
// Should create a layer without error
// Test specific origins
let cors_specific = Some("http://localhost:3000,https://admin.example.com".to_string());
let _layer2 = parse_cors_origins(cors_specific.as_ref());
// Should create a layer without error
// Test empty origin
let cors_empty = Some("".to_string());
let _layer3 = parse_cors_origins(cors_empty.as_ref());
// Should create a layer without error (falls back to permissive)
// Test no origin
let _layer4 = parse_cors_origins(None);
// Should create a layer without error (uses default)
}
#[tokio::test]
async fn test_external_address_configuration() {
// Test external address configuration
let args = vec![
"rustfs",
"/tmp/test",
"--console-address",
":9001",
"--external-address",
":9020",
];
let opt = Opt::parse_from(args);
assert_eq!(opt.console_address, ":9001");
assert_eq!(opt.external_address, ":9020".to_string());
}
#[tokio::test]
async fn test_console_tls_configuration() {
// Test TLS configuration options (now uses shared tls_path)
let args = vec!["rustfs", "/tmp/test", "--tls-path", "/path/to/tls"];
let opt = Opt::parse_from(args);
assert_eq!(opt.tls_path, Some("/path/to/tls".to_string()));
}
#[tokio::test]
async fn test_console_health_check_endpoint() {
// Test that console health check can be called
// This test would need a running server to be comprehensive
// For now, we test configuration and startup behavior
let args = vec!["rustfs", "/tmp/test", "--console-address", ":0"];
let opt = Opt::parse_from(args);
// Verify the configuration supports health checks
assert!(opt.console_enable, "Console should be enabled for health checks");
}
#[tokio::test]
async fn test_console_separate_logging_target() {
// Test that console uses separate logging targets
use tracing::info;
// This test verifies that logging targets are properly set up
info!(target: "rustfs::console::startup", "Test console startup log");
info!(target: "rustfs::console::access", "Test console access log");
info!(target: "rustfs::console::error", "Test console error log");
info!(target: "rustfs::console::shutdown", "Test console shutdown log");
// In a real implementation, we would verify these logs are captured separately
}
#[tokio::test]
async fn test_console_configuration_validation() {
// Test configuration validation
let args = vec![
"rustfs",
"/tmp/test",
"--console-enable",
"true",
"--console-address",
":9001",
"--external-address",
":9020",
];
let opt = Opt::parse_from(args);
// Verify all console-related configuration is parsed correctly
assert!(opt.console_enable);
assert_eq!(opt.console_address, ":9001");
assert_eq!(opt.external_address, ":9020".to_string());
}
}

View File

@@ -46,19 +46,89 @@ use tokio_rustls::TlsAcceptor;
use tonic::{Request, Status, metadata::MetadataValue};
use tower::ServiceBuilder;
use tower_http::catch_panic::CatchPanicLayer;
use tower_http::cors::CorsLayer;
use tower_http::cors::{AllowOrigin, Any, CorsLayer};
use tower_http::trace::TraceLayer;
use tracing::{Span, debug, error, info, instrument, warn};
const MI_B: usize = 1024 * 1024;
/// Parse CORS allowed origins from configuration
fn parse_cors_origins(origins: Option<&String>) -> CorsLayer {
use http::Method;
let cors_layer = CorsLayer::new()
.allow_methods([
Method::GET,
Method::POST,
Method::PUT,
Method::DELETE,
Method::HEAD,
Method::OPTIONS,
])
.allow_headers([
http::header::CONTENT_TYPE,
http::header::AUTHORIZATION,
http::header::ACCEPT,
http::header::ORIGIN,
// Note: X_AMZ_* headers are custom and may need to be defined
// http::header::X_AMZ_CONTENT_SHA256,
// http::header::X_AMZ_DATE,
// http::header::X_AMZ_SECURITY_TOKEN,
// http::header::X_AMZ_USER_AGENT,
http::header::RANGE,
]);
match origins {
Some(origins_str) if origins_str == "*" => cors_layer.allow_origin(Any),
Some(origins_str) => {
let origins: Vec<&str> = origins_str.split(',').map(|s| s.trim()).collect();
if origins.is_empty() {
warn!("Empty CORS origins provided, using permissive CORS");
cors_layer.allow_origin(Any)
} else {
// Parse origins with proper error handling
let mut valid_origins = Vec::new();
for origin in origins {
match origin.parse::<http::HeaderValue>() {
Ok(header_value) => {
valid_origins.push(header_value);
}
Err(e) => {
warn!("Invalid CORS origin '{}': {}", origin, e);
}
}
}
if valid_origins.is_empty() {
warn!("No valid CORS origins found, using permissive CORS");
cors_layer.allow_origin(Any)
} else {
info!("Endpoint CORS origins configured: {:?}", valid_origins);
cors_layer.allow_origin(AllowOrigin::list(valid_origins))
}
}
}
None => {
debug!("No CORS origins configured for endpoint, using permissive CORS");
cors_layer.allow_origin(Any)
}
}
}
fn get_cors_allowed_origins() -> String {
std::env::var(rustfs_config::ENV_CORS_ALLOWED_ORIGINS)
.unwrap_or_else(|_| rustfs_config::DEFAULT_CORS_ALLOWED_ORIGINS.to_string())
.parse::<String>()
.unwrap_or(rustfs_config::DEFAULT_CONSOLE_CORS_ALLOWED_ORIGINS.to_string())
}
pub async fn start_http_server(
opt: &config::Opt,
worker_state_manager: ServiceStateManager,
) -> Result<tokio::sync::broadcast::Sender<()>> {
let server_addr = parse_and_resolve_address(opt.address.as_str()).map_err(Error::other)?;
let server_port = server_addr.port();
let server_address = server_addr.to_string();
let _server_address = server_addr.to_string();
// The listening address and port are obtained from the parameters
let listener = {
@@ -107,12 +177,6 @@ pub async fn start_http_server(
let api_endpoints = format!("http://{local_ip}:{server_port}");
let localhost_endpoint = format!("http://127.0.0.1:{server_port}");
info!(" API: {} {}", api_endpoints, localhost_endpoint);
if opt.console_enable {
info!(
" WebUI: http://{}:{}/rustfs/console/index.html http://127.0.0.1:{}/rustfs/console/index.html http://{}/rustfs/console/index.html",
local_ip, server_port, server_port, server_address
);
}
info!(" RootUser: {}", opt.access_key.clone());
info!(" RootPass: {}", opt.secret_key.clone());
if DEFAULT_ACCESS_KEY.eq(&opt.access_key) && DEFAULT_SECRET_KEY.eq(&opt.secret_key) {
@@ -134,7 +198,9 @@ pub async fn start_http_server(
b.set_auth(IAMAuth::new(access_key, secret_key));
b.set_access(store.clone());
b.set_route(admin::make_admin_route(opt.console_enable)?);
// When console runs on separate port, disable console routes on main endpoint
let console_on_endpoint = false; // Console will run separately
b.set_route(admin::make_admin_route(console_on_endpoint)?);
if !opt.server_domains.is_empty() {
MultiDomain::new(&opt.server_domains).map_err(Error::other)?; // validate domains
@@ -178,7 +244,17 @@ pub async fn start_http_server(
let (shutdown_tx, mut shutdown_rx) = tokio::sync::broadcast::channel(1);
let shutdown_tx_clone = shutdown_tx.clone();
// Capture CORS configuration for the server loop
let cors_allowed_origins = get_cors_allowed_origins();
let cors_allowed_origins = if cors_allowed_origins.is_empty() {
None
} else {
Some(cors_allowed_origins)
};
tokio::spawn(async move {
// Create CORS layer inside the server loop closure
let cors_layer = parse_cors_origins(cors_allowed_origins.as_ref());
#[cfg(unix)]
let (mut sigterm_inner, mut sigint_inner) = {
use tokio::signal::unix::{SignalKind, signal};
@@ -265,7 +341,14 @@ pub async fn start_http_server(
warn!(?err, "Failed to set set_send_buffer_size");
}
process_connection(socket, tls_acceptor.clone(), http_server.clone(), s3_service.clone(), graceful.clone());
process_connection(
socket,
tls_acceptor.clone(),
http_server.clone(),
s3_service.clone(),
graceful.clone(),
cors_layer.clone(),
);
}
worker_state_manager.update(ServiceState::Stopping);
@@ -372,6 +455,7 @@ fn process_connection(
http_server: Arc<ConnBuilder<TokioExecutor>>,
s3_service: S3Service,
graceful: Arc<GracefulShutdown>,
cors_layer: CorsLayer,
) {
tokio::spawn(async move {
// Build services inside each connected task to avoid passing complex service types across tasks,
@@ -423,7 +507,7 @@ fn process_connection(
debug!("http request failure error: {:?} in {:?}", _error, latency)
}),
)
.layer(CorsLayer::permissive())
.layer(cors_layer)
.layer(RedirectLayer)
.service(service);
let hybrid_service = TowerToHyperService::new(hybrid_service);

View File

@@ -13,11 +13,16 @@
// limitations under the License.
mod audit;
pub mod console;
mod http;
mod hybrid;
mod layer;
mod service_state;
#[cfg(test)]
mod console_test;
pub(crate) use console::start_console_server;
pub(crate) use http::start_http_server;
pub(crate) use service_state::SHUTDOWN_TIMEOUT;
pub(crate) use service_state::ServiceState;

View File

@@ -73,15 +73,15 @@ pub(crate) async fn wait_for_shutdown() -> ShutdownSignal {
tokio::select! {
_ = tokio::signal::ctrl_c() => {
info!("Received Ctrl-C signal");
info!("RustFS Received Ctrl-C signal");
ShutdownSignal::CtrlC
}
_ = sigint.recv() => {
info!("Received SIGINT signal");
info!("RustFS Received SIGINT signal");
ShutdownSignal::Sigint
}
_ = sigterm.recv() => {
info!("Received SIGTERM signal");
info!("RustFS Received SIGTERM signal");
ShutdownSignal::Sigterm
}
}
@@ -121,7 +121,7 @@ impl ServiceStateManager {
fn notify_systemd(&self, state: &ServiceState) {
match state {
ServiceState::Starting => {
info!("Service is starting...");
info!("RustFS Service is starting...");
#[cfg(target_os = "linux")]
if let Err(e) =
libsystemd::daemon::notify(false, &[libsystemd::daemon::NotifyState::Status("Starting...".to_string())])
@@ -130,15 +130,15 @@ impl ServiceStateManager {
}
}
ServiceState::Ready => {
info!("Service is ready");
info!("RustFS Service is ready");
notify_systemd("ready");
}
ServiceState::Stopping => {
info!("Service is stopping...");
info!("RustFS Service is stopping...");
notify_systemd("stopping");
}
ServiceState::Stopped => {
info!("Service has stopped");
info!("RustFS Service has stopped");
#[cfg(target_os = "linux")]
if let Err(e) =
libsystemd::daemon::notify(false, &[libsystemd::daemon::NotifyState::Status("Stopped".to_string())])

View File

@@ -45,7 +45,8 @@ export RUSTFS_VOLUMES="./target/volume/test{1...4}"
# export RUSTFS_VOLUMES="./target/volume/test"
export RUSTFS_ADDRESS=":9000"
export RUSTFS_CONSOLE_ENABLE=true
# export RUSTFS_CONSOLE_ADDRESS=":9001"
export RUSTFS_CONSOLE_ADDRESS=":9001"
export RUSTFS_EXTERNAL_ADDRESS=":9020"
# export RUSTFS_SERVER_DOMAINS="localhost:9000"
# HTTPS certificate directory
# export RUSTFS_TLS_PATH="./deploy/certs"