mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-08 14:23:13 +00:00
9059a9c68d
* refactor(logging): standardize concurrency and proxy events * chore(logging): extend guardrails for concurrency and proxies * feat(skill): add rustfs logging governance skill
389 lines
14 KiB
Rust
389 lines
14 KiB
Rust
// 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.
|
|
|
|
//! Cloud provider detection and metadata fetching.
|
|
|
|
use async_trait::async_trait;
|
|
use rustfs_utils::get_env_opt_str;
|
|
use std::str::FromStr;
|
|
use std::time::Duration;
|
|
use tracing::{debug, info, warn};
|
|
|
|
use crate::AppError;
|
|
|
|
/// Supported cloud providers.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
pub enum CloudProvider {
|
|
/// Amazon Web Services
|
|
Aws,
|
|
/// Microsoft Azure
|
|
Azure,
|
|
/// Google Cloud Platform
|
|
Gcp,
|
|
/// DigitalOcean
|
|
DigitalOcean,
|
|
/// Cloudflare
|
|
Cloudflare,
|
|
/// Unknown or custom provider.
|
|
Unknown(String),
|
|
}
|
|
|
|
impl FromStr for CloudProvider {
|
|
type Err = std::convert::Infallible;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
Ok(match s.to_lowercase().as_str() {
|
|
"aws" | "amazon" => Self::Aws,
|
|
"azure" | "microsoft" => Self::Azure,
|
|
"gcp" | "google" => Self::Gcp,
|
|
"digitalocean" | "do" => Self::DigitalOcean,
|
|
"cloudflare" | "cf" => Self::Cloudflare,
|
|
_ => Self::Unknown(s.to_string()),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl CloudProvider {
|
|
/// Detects the cloud provider based on environment variables.
|
|
pub fn detect_from_env() -> Option<Self> {
|
|
let has_env = |key| get_env_opt_str(key).is_some();
|
|
|
|
// Check for AWS environment variables.
|
|
if has_env("RUSTFS_AWS_EXECUTION_ENV") || has_env("RUSTFS_AWS_REGION") || has_env("RUSTFS_EC2_INSTANCE_ID") {
|
|
return Some(Self::Aws);
|
|
}
|
|
|
|
// Check for Azure environment variables.
|
|
if has_env("RUSTFS_WEBSITE_SITE_NAME")
|
|
|| has_env("RUSTFS_WEBSITE_INSTANCE_ID")
|
|
|| has_env("RUSTFS_APPSETTING_WEBSITE_SITE_NAME")
|
|
{
|
|
return Some(Self::Azure);
|
|
}
|
|
|
|
// Check for GCP environment variables.
|
|
if has_env("RUSTFS_GCP_PROJECT") || has_env("RUSTFS_GOOGLE_CLOUD_PROJECT") || has_env("RUSTFS_GAE_INSTANCE") {
|
|
return Some(Self::Gcp);
|
|
}
|
|
|
|
// Check for DigitalOcean environment variables.
|
|
if has_env("RUSTFS_DIGITALOCEAN_REGION") {
|
|
return Some(Self::DigitalOcean);
|
|
}
|
|
|
|
// Check for Cloudflare environment variables.
|
|
if has_env("RUSTFS_CF_PAGES") || has_env("RUSTFS_CF_WORKERS") {
|
|
return Some(Self::Cloudflare);
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Returns the canonical name of the cloud provider.
|
|
pub fn name(&self) -> &str {
|
|
match self {
|
|
Self::Aws => "aws",
|
|
Self::Azure => "azure",
|
|
Self::Gcp => "gcp",
|
|
Self::DigitalOcean => "digitalocean",
|
|
Self::Cloudflare => "cloudflare",
|
|
Self::Unknown(name) => name,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use temp_env::{with_var, with_vars_unset};
|
|
|
|
#[test]
|
|
fn test_detect_from_env_prefers_known_provider_markers() {
|
|
with_var("RUSTFS_AWS_REGION", Some("us-east-1"), || {
|
|
assert_eq!(CloudProvider::detect_from_env(), Some(CloudProvider::Aws));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_detect_from_env_returns_none_without_markers() {
|
|
with_vars_unset(
|
|
vec![
|
|
"RUSTFS_AWS_EXECUTION_ENV",
|
|
"RUSTFS_AWS_REGION",
|
|
"RUSTFS_EC2_INSTANCE_ID",
|
|
"RUSTFS_WEBSITE_SITE_NAME",
|
|
"RUSTFS_WEBSITE_INSTANCE_ID",
|
|
"RUSTFS_APPSETTING_WEBSITE_SITE_NAME",
|
|
"RUSTFS_GCP_PROJECT",
|
|
"RUSTFS_GOOGLE_CLOUD_PROJECT",
|
|
"RUSTFS_GAE_INSTANCE",
|
|
"RUSTFS_DIGITALOCEAN_REGION",
|
|
"RUSTFS_CF_PAGES",
|
|
"RUSTFS_CF_WORKERS",
|
|
],
|
|
|| {
|
|
assert_eq!(CloudProvider::detect_from_env(), None);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Trait for fetching metadata from a specific cloud provider.
|
|
#[async_trait]
|
|
pub trait CloudMetadataFetcher: Send + Sync {
|
|
/// Returns the name of the provider.
|
|
fn provider_name(&self) -> &str;
|
|
|
|
/// Fetches the network CIDR ranges for the current instance.
|
|
async fn fetch_network_cidrs(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError>;
|
|
|
|
/// Fetches the public IP ranges for the cloud provider.
|
|
async fn fetch_public_ip_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError>;
|
|
|
|
/// Fetches all IP ranges that should be considered trusted proxies.
|
|
async fn fetch_trusted_proxy_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
|
|
let mut ranges = Vec::new();
|
|
|
|
match self.fetch_network_cidrs().await {
|
|
Ok(cidrs) => ranges.extend(cidrs),
|
|
Err(e) => warn!(
|
|
event = "trusted_proxies.cloud_fetch",
|
|
component = "trusted_proxies",
|
|
subsystem = "cloud_detector",
|
|
provider = self.provider_name(),
|
|
dataset = "network_cidrs",
|
|
result = "degraded",
|
|
error = %e,
|
|
"trusted proxy cloud metadata fetch degraded"
|
|
),
|
|
}
|
|
|
|
match self.fetch_public_ip_ranges().await {
|
|
Ok(public_ranges) => ranges.extend(public_ranges),
|
|
Err(e) => warn!(
|
|
event = "trusted_proxies.cloud_fetch",
|
|
component = "trusted_proxies",
|
|
subsystem = "cloud_detector",
|
|
provider = self.provider_name(),
|
|
dataset = "public_ip_ranges",
|
|
result = "degraded",
|
|
error = %e,
|
|
"trusted proxy cloud metadata fetch degraded"
|
|
),
|
|
}
|
|
|
|
Ok(ranges)
|
|
}
|
|
}
|
|
|
|
/// Detector for identifying the current cloud environment and fetching relevant metadata.
|
|
#[derive(Debug, Clone)]
|
|
pub struct CloudDetector {
|
|
/// Whether cloud detection is enabled.
|
|
enabled: bool,
|
|
/// Timeout for metadata requests.
|
|
timeout: Duration,
|
|
/// Optionally force a specific provider.
|
|
forced_provider: Option<CloudProvider>,
|
|
}
|
|
|
|
impl CloudDetector {
|
|
/// Creates a new `CloudDetector`.
|
|
pub fn new(enabled: bool, timeout: Duration, forced_provider: Option<String>) -> Self {
|
|
let forced_provider = forced_provider.and_then(|s| CloudProvider::from_str(&s).ok());
|
|
|
|
Self {
|
|
enabled,
|
|
timeout,
|
|
forced_provider,
|
|
}
|
|
}
|
|
|
|
/// Identifies the current cloud provider.
|
|
pub fn detect_provider(&self) -> Option<CloudProvider> {
|
|
if !self.enabled {
|
|
return None;
|
|
}
|
|
|
|
if let Some(provider) = self.forced_provider.as_ref() {
|
|
return Some(provider.clone());
|
|
}
|
|
|
|
CloudProvider::detect_from_env()
|
|
}
|
|
|
|
/// Fetches trusted IP ranges for the detected cloud provider.
|
|
pub async fn fetch_trusted_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
|
|
if !self.enabled {
|
|
debug!(
|
|
event = "trusted_proxies.cloud_detect",
|
|
component = "trusted_proxies",
|
|
subsystem = "cloud_detector",
|
|
state = "disabled",
|
|
"trusted proxy cloud detection skipped"
|
|
);
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
let provider = self.detect_provider();
|
|
|
|
match provider {
|
|
Some(CloudProvider::Aws) => {
|
|
info!(
|
|
event = "trusted_proxies.cloud_detect",
|
|
component = "trusted_proxies",
|
|
subsystem = "cloud_detector",
|
|
provider = "aws",
|
|
result = "detected",
|
|
timeout_ms = self.timeout.as_millis(),
|
|
"trusted proxy cloud provider detected"
|
|
);
|
|
let fetcher = crate::AwsMetadataFetcher::new(self.timeout);
|
|
fetcher.fetch_trusted_proxy_ranges().await
|
|
}
|
|
Some(CloudProvider::Azure) => {
|
|
info!(
|
|
event = "trusted_proxies.cloud_detect",
|
|
component = "trusted_proxies",
|
|
subsystem = "cloud_detector",
|
|
provider = "azure",
|
|
result = "detected",
|
|
timeout_ms = self.timeout.as_millis(),
|
|
"trusted proxy cloud provider detected"
|
|
);
|
|
let fetcher = crate::AzureMetadataFetcher::new(self.timeout);
|
|
fetcher.fetch_trusted_proxy_ranges().await
|
|
}
|
|
Some(CloudProvider::Gcp) => {
|
|
info!(
|
|
event = "trusted_proxies.cloud_detect",
|
|
component = "trusted_proxies",
|
|
subsystem = "cloud_detector",
|
|
provider = "gcp",
|
|
result = "detected",
|
|
timeout_ms = self.timeout.as_millis(),
|
|
"trusted proxy cloud provider detected"
|
|
);
|
|
let fetcher = crate::GcpMetadataFetcher::new(self.timeout);
|
|
fetcher.fetch_trusted_proxy_ranges().await
|
|
}
|
|
Some(CloudProvider::Cloudflare) => {
|
|
info!(
|
|
event = "trusted_proxies.cloud_detect",
|
|
component = "trusted_proxies",
|
|
subsystem = "cloud_detector",
|
|
provider = "cloudflare",
|
|
result = "detected",
|
|
"trusted proxy cloud provider detected"
|
|
);
|
|
let ranges = crate::CloudflareIpRanges::fetch().await?;
|
|
Ok(ranges)
|
|
}
|
|
Some(CloudProvider::DigitalOcean) => {
|
|
info!(
|
|
event = "trusted_proxies.cloud_detect",
|
|
component = "trusted_proxies",
|
|
subsystem = "cloud_detector",
|
|
provider = "digitalocean",
|
|
result = "detected",
|
|
"trusted proxy cloud provider detected"
|
|
);
|
|
let ranges = crate::DigitalOceanIpRanges::fetch().await?;
|
|
Ok(ranges)
|
|
}
|
|
Some(CloudProvider::Unknown(name)) => {
|
|
warn!(
|
|
event = "trusted_proxies.cloud_detect",
|
|
component = "trusted_proxies",
|
|
subsystem = "cloud_detector",
|
|
provider = %name,
|
|
result = "unknown",
|
|
"trusted proxy cloud provider unresolved"
|
|
);
|
|
Ok(Vec::new())
|
|
}
|
|
None => {
|
|
debug!(
|
|
event = "trusted_proxies.cloud_detect",
|
|
component = "trusted_proxies",
|
|
subsystem = "cloud_detector",
|
|
result = "none",
|
|
"trusted proxy cloud provider not detected"
|
|
);
|
|
Ok(Vec::new())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Attempts to fetch metadata from all supported providers sequentially.
|
|
pub async fn try_all_providers(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
|
|
if !self.enabled {
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
let providers: Vec<Box<dyn CloudMetadataFetcher>> = vec![
|
|
Box::new(crate::AwsMetadataFetcher::new(self.timeout)),
|
|
Box::new(crate::AzureMetadataFetcher::new(self.timeout)),
|
|
Box::new(crate::GcpMetadataFetcher::new(self.timeout)),
|
|
];
|
|
|
|
for provider in providers {
|
|
let provider_name = provider.provider_name();
|
|
debug!(
|
|
event = "trusted_proxies.cloud_detect",
|
|
component = "trusted_proxies",
|
|
subsystem = "cloud_detector",
|
|
provider = provider_name,
|
|
result = "attempt",
|
|
"trusted proxy cloud provider fetch attempted"
|
|
);
|
|
|
|
match provider.fetch_trusted_proxy_ranges().await {
|
|
Ok(ranges) => {
|
|
if !ranges.is_empty() {
|
|
info!(
|
|
event = "trusted_proxies.cloud_detect",
|
|
component = "trusted_proxies",
|
|
subsystem = "cloud_detector",
|
|
provider = provider_name,
|
|
result = "loaded",
|
|
range_count = ranges.len(),
|
|
"trusted proxy cloud provider ranges loaded"
|
|
);
|
|
return Ok(ranges);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
debug!(
|
|
event = "trusted_proxies.cloud_detect",
|
|
component = "trusted_proxies",
|
|
subsystem = "cloud_detector",
|
|
provider = provider_name,
|
|
result = "failed",
|
|
error = %e,
|
|
"trusted proxy cloud provider fetch failed"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(Vec::new())
|
|
}
|
|
}
|
|
|
|
/// Returns a default `CloudDetector` with detection disabled.
|
|
pub fn default_cloud_detector() -> CloudDetector {
|
|
CloudDetector::new(false, Duration::from_secs(5), None)
|
|
}
|