feat(tls): add inspect command for TLS layouts (#3092)

This commit is contained in:
houseme
2026-05-27 15:23:58 +08:00
committed by GitHub
parent 909f0d57fb
commit ceffe21f75
9 changed files with 371 additions and 7 deletions
+29 -2
View File
@@ -16,9 +16,10 @@
//!
//! This module contains the command-line interface definitions including:
//! - `Cli`: Main CLI parser
//! - `Commands`: Subcommands (Server, Info)
//! - `Commands`: Subcommands (Server, Info, Tls)
//! - `ServerOpts`: Server subcommand options
//! - `InfoOpts`: Info subcommand options
//! - `TlsOpts`: TLS diagnostic subcommand options
//! - `InfoType`: Information type enum
//! - `CommandResult`: Result of parsing command line arguments
@@ -55,7 +56,7 @@ pub(super) const LONG_VERSION: &str = concat!(
);
/// Known subcommands. When the first arg matches one of these, it is treated as a subcommand.
pub const KNOWN_SUBCOMMANDS: &[&str] = &["server", "info"];
pub const KNOWN_SUBCOMMANDS: &[&str] = &["server", "info", "tls"];
/// Preprocess argv for legacy compatibility: `rustfs <volume>` and `rustfs --address ...` are
/// treated as `rustfs server <volume>` and `rustfs server --address ...` respectively.
@@ -102,6 +103,8 @@ pub enum Commands {
Server(Box<ServerOpts>),
/// Display system information
Info(InfoOpts),
/// Inspect TLS certificate directory layout and parsing status
Tls(TlsOpts),
}
/// Information type to display
@@ -135,6 +138,28 @@ pub struct InfoOpts {
pub info_type: Option<InfoType>,
}
/// TLS diagnostic subcommand options
#[derive(Args, Clone)]
pub struct TlsOpts {
#[command(subcommand)]
pub command: TlsCommands,
}
/// TLS diagnostic subcommands
#[derive(Subcommand, Clone)]
pub enum TlsCommands {
/// Inspect a TLS certificate directory
Inspect(TlsInspectOpts),
}
/// TLS inspect options
#[derive(Args, Clone)]
pub struct TlsInspectOpts {
/// TLS directory to inspect
#[arg(long, value_parser = NonEmptyStringValueParser::new())]
pub path: String,
}
/// Server subcommand options
#[derive(Args, Clone)]
pub struct ServerOpts {
@@ -263,6 +288,8 @@ pub enum CommandResult {
Server(Box<super::Config>),
/// Info command with options
Info(InfoOpts),
/// TLS command with options
Tls(TlsOpts),
}
/// Create default ServerOpts from environment variables
+15 -1
View File
@@ -15,7 +15,7 @@
#[cfg(test)]
#[allow(unsafe_op_in_unsafe_fn)]
mod tests {
use crate::config::{Config, Opt};
use crate::config::{CommandResult, Config, Opt, TlsCommands};
use rustfs_config::{DEFAULT_CONSOLE_ADDRESS, DEFAULT_CONSOLE_ENABLE, DEFAULT_OBS_ENDPOINT, RUSTFS_REGION};
use rustfs_credentials::{DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY};
use rustfs_ecstore::disks_layout::DisksLayout;
@@ -72,6 +72,20 @@ mod tests {
assert_eq!(opt_legacy.console_address, opt_server.console_address);
}
#[test]
#[serial]
fn test_tls_inspect_subcommand_parses() {
let result =
Opt::parse_command(["rustfs", "tls", "inspect", "--path", "/tmp/certs"]).expect("tls inspect command should parse");
match result {
CommandResult::Tls(opts) => match opts.command {
TlsCommands::Inspect(inspect) => assert_eq!(inspect.path, "/tmp/certs"),
},
_ => panic!("expected TLS command result"),
}
}
#[test]
#[serial]
fn test_default_console_configuration() {
+1
View File
@@ -51,6 +51,7 @@ mod config_test;
// Re-export public types
pub use cli::{CommandResult, InfoOpts, InfoType};
pub use cli::{TlsCommands, TlsInspectOpts, TlsOpts};
pub use config_struct::Config;
pub use info::execute_info;
pub use opt::Opt;
+5 -1
View File
@@ -98,6 +98,9 @@ impl Opt {
// This should not happen in parse_from, as it's handled by parse_command
panic!("Info command should be handled by parse_command");
}
Some(Commands::Tls(_)) => {
panic!("TLS command should be handled by parse_command");
}
None => {
// Default to server with empty volumes (will be filled from env)
Self::from_server_opts(default_server_opts())
@@ -129,6 +132,7 @@ impl Opt {
};
match cli.command {
Some(Commands::Info(opts)) => Ok(CommandResult::Info(opts)),
Some(Commands::Tls(opts)) => Ok(CommandResult::Tls(opts)),
Some(Commands::Server(opts)) => Self::server_command_result(Self::from_server_opts(*opts)),
None => {
// Default to server with empty volumes (will be filled from env)
@@ -157,7 +161,7 @@ impl Opt {
let cli = Cli::try_parse_from(args)?;
match cli.command {
Some(Commands::Server(opts)) => Ok(Self::from_server_opts(*opts)),
Some(Commands::Info(_)) => Err(clap::Error::new(clap::error::ErrorKind::DisplayHelp)),
Some(Commands::Info(_)) | Some(Commands::Tls(_)) => Err(clap::Error::new(clap::error::ErrorKind::DisplayHelp)),
None => {
// Default to server with empty volumes
Ok(Self::from_server_opts(default_server_opts()))
+1
View File
@@ -69,6 +69,7 @@ pub mod protocols;
pub mod server;
pub mod startup_fs_guard;
pub mod storage;
pub mod tls;
pub mod update;
pub mod version;
+6 -1
View File
@@ -159,10 +159,15 @@ async fn async_main() -> Result<()> {
return Ok(());
}
if let rustfs::config::CommandResult::Tls(opts) = &command_result {
rustfs::tls::execute_tls(opts)?;
return Ok(());
}
// Get config for server command
let config = match command_result {
rustfs::config::CommandResult::Server(cfg) => cfg,
rustfs::config::CommandResult::Info(_) => unreachable!(),
rustfs::config::CommandResult::Info(_) | rustfs::config::CommandResult::Tls(_) => unreachable!(),
};
// Initialize the global config snapshot for info command
+135
View File
@@ -0,0 +1,135 @@
// 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::config::{TlsCommands, TlsInspectOpts, TlsOpts};
use rustfs_config::{RUSTFS_TLS_CERT, RUSTFS_TLS_KEY};
use rustfs_tls_runtime::{CertDirectoryLoadOptions, TlsCertPairInspection, TlsCertPairStatus, TlsSource, inspect_cert_directory};
use std::io::{Error, Result};
pub fn execute_tls(opts: &TlsOpts) -> Result<()> {
match &opts.command {
TlsCommands::Inspect(inspect) => execute_tls_inspect(inspect),
}
}
fn execute_tls_inspect(opts: &TlsInspectOpts) -> Result<()> {
let tls_source = TlsSource::from_directory(opts.path.trim());
let tls_dir = tls_source.validate_directory().map_err(Error::other)?;
let inspection = inspect_cert_directory(CertDirectoryLoadOptions::builder(tls_dir, RUSTFS_TLS_CERT, RUSTFS_TLS_KEY).build())?;
println!("TLS directory inspection");
println!("path: {}", inspection.directory.display());
if let Some(canonical_directory) = &inspection.canonical_directory {
println!("canonical path: {}", canonical_directory.display());
}
println!("root pair: {}", format_pair_summary(&inspection.root_pair));
let valid_domains = inspection.valid_domain_names();
let runtime_mode = if !valid_domains.is_empty() {
"multi-cert/SNI"
} else if inspection.has_valid_root_pair() {
"single-cert/default"
} else {
"no-usable-server-cert"
};
println!("runtime mode: {}", runtime_mode);
println!("valid domain pairs: {}", valid_domains.len());
if !valid_domains.is_empty() {
println!("valid domains: {}", valid_domains.join(", "));
}
if !inspection.skipped_directory_names.is_empty() {
println!("skipped internal directories: {}", inspection.skipped_directory_names.join(", "));
}
if inspection.domain_pairs.is_empty() {
println!("domain pairs: none");
} else {
println!("domain pairs:");
for domain in &inspection.domain_pairs {
println!(" - {}: {}", domain.domain_name, format_pair_summary(&domain.pair));
}
}
Ok(())
}
fn format_pair_summary(pair: &TlsCertPairInspection) -> String {
match &pair.status {
TlsCertPairStatus::MissingBoth => format!(
"missing cert/key (expected cert={}, key={})",
pair.cert_path.display(),
pair.key_path.display()
),
TlsCertPairStatus::MissingCert => format!(
"missing cert (expected cert={}, key={})",
pair.cert_path.display(),
pair.key_path.display()
),
TlsCertPairStatus::MissingKey => format!(
"missing key (expected cert={}, key={})",
pair.cert_path.display(),
pair.key_path.display()
),
TlsCertPairStatus::Valid => {
format!("valid cert/key pair (cert={}, key={})", pair.cert_path.display(), pair.key_path.display())
}
TlsCertPairStatus::Invalid { error } => format!(
"invalid cert/key pair (cert={}, key={}): {}",
pair.cert_path.display(),
pair.key_path.display(),
error
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn write_test_cert_pair(dir: &std::path::Path) {
let rcgen::CertifiedKey { cert, signing_key } =
rcgen::generate_simple_self_signed(vec!["example.com".to_string()]).expect("cert should generate");
fs::write(dir.join(RUSTFS_TLS_CERT), cert.pem()).expect("cert should write");
fs::write(dir.join(RUSTFS_TLS_KEY), signing_key.serialize_pem()).expect("key should write");
}
#[test]
fn execute_tls_inspect_accepts_valid_directory() {
let temp_dir = TempDir::new().expect("tempdir should create");
write_test_cert_pair(temp_dir.path());
let opts = TlsInspectOpts {
path: temp_dir.path().display().to_string(),
};
execute_tls_inspect(&opts).expect("tls inspect should succeed");
}
#[test]
fn format_pair_summary_reports_missing_key() {
let pair = TlsCertPairInspection {
cert_path: "/tmp/rustfs_cert.pem".into(),
key_path: "/tmp/rustfs_key.pem".into(),
status: TlsCertPairStatus::MissingKey,
};
let summary = format_pair_summary(&pair);
assert!(summary.contains("missing key"));
assert!(summary.contains("/tmp/rustfs_cert.pem"));
assert!(summary.contains("/tmp/rustfs_key.pem"));
}
}