mirror of
https://github.com/rustfs/rustfs.git
synced 2026-01-16 17:20:33 +00:00
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>
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -20,4 +20,6 @@ profile.json
|
||||
.docker/openobserve-otel/data
|
||||
*.zst
|
||||
.secrets
|
||||
*.go
|
||||
*.go
|
||||
*.pb
|
||||
*.svg
|
||||
574
Cargo.lock
generated
574
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
31
Cargo.toml
31
Cargo.toml
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
81
crates/config/src/constants/console.rs
Normal file
81
crates/config/src/constants/console.rs
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -616,6 +616,7 @@ impl ServerHandler for RustfsMcpServer {
|
||||
server_info: Implementation {
|
||||
name: "rustfs-mcp-server".into(),
|
||||
version: env!("CARGO_PKG_VERSION").into(),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
1362
docs/console-separation.md
Normal file
1362
docs/console-separation.md
Normal file
File diff suppressed because it is too large
Load Diff
270
examples/README.md
Normal file
270
examples/README.md
Normal 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)
|
||||
224
examples/docker-comprehensive.yml
Normal file
224
examples/docker-comprehensive.yml
Normal 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
295
examples/docker-quickstart.sh
Executable 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
|
||||
321
examples/enhanced-docker-deployment.sh
Executable file
321
examples/enhanced-docker-deployment.sh
Executable 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
|
||||
207
examples/enhanced-security-deployment.sh
Executable file
207
examples/enhanced-security-deployment.sh
Executable 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 "$@"
|
||||
@@ -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 }
|
||||
|
||||
@@ -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")
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {}))?;
|
||||
|
||||
|
||||
79
rustfs/src/config/config_test.rs
Normal file
79
rustfs/src/config/config_test.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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>,
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
398
rustfs/src/server/console.rs
Normal file
398
rustfs/src/server/console.rs
Normal 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(())
|
||||
}
|
||||
146
rustfs/src/server/console_test.rs
Normal file
146
rustfs/src/server/console_test.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())])
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user