P0.4: Scaffold miroir-ctl crate
Add miroir-ctl management CLI with: - clap root CLI with admin-key loading (env → credentials file → flag) - All 15 subcommand stubs from plan §4 - Unit tests for credential loader priority order - Clear "not yet implemented" messages pointing to tracking bead Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
fe274a5c0e
commit
78e5fe1acb
20 changed files with 644 additions and 2 deletions
|
|
@ -11,7 +11,7 @@ path = "src/main.rs"
|
|||
|
||||
[dependencies]
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.42", features = ["full"] }
|
||||
|
|
|
|||
15
crates/miroir-ctl/src/commands/alias.rs
Normal file
15
crates/miroir-ctl/src/commands/alias.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use clap::Subcommand;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum AliasSubcommand {
|
||||
/// Create a new alias
|
||||
Create,
|
||||
/// Delete an alias
|
||||
Delete,
|
||||
/// List all aliases
|
||||
List,
|
||||
}
|
||||
|
||||
pub async fn run(_cmd: AliasSubcommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Err("This command is not yet implemented. See bead miroir-qon for tracking.".into())
|
||||
}
|
||||
17
crates/miroir-ctl/src/commands/canary.rs
Normal file
17
crates/miroir-ctl/src/commands/canary.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
use clap::Subcommand;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum CanarySubcommand {
|
||||
/// Create a canary deployment
|
||||
Create,
|
||||
/// Promote a canary to primary
|
||||
Promote,
|
||||
/// Rollback a canary
|
||||
Rollback,
|
||||
/// Show canary status
|
||||
Status,
|
||||
}
|
||||
|
||||
pub async fn run(_cmd: CanarySubcommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Err("This command is not yet implemented. See bead miroir-qon for tracking.".into())
|
||||
}
|
||||
15
crates/miroir-ctl/src/commands/cdc.rs
Normal file
15
crates/miroir-ctl/src/commands/cdc.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use clap::Subcommand;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum CdcSubcommand {
|
||||
/// Create a CDC subscription
|
||||
Create,
|
||||
/// List CDC subscriptions
|
||||
List,
|
||||
/// Delete a CDC subscription
|
||||
Delete,
|
||||
}
|
||||
|
||||
pub async fn run(_cmd: CdcSubcommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Err("This command is not yet implemented. See bead miroir-qon for tracking.".into())
|
||||
}
|
||||
15
crates/miroir-ctl/src/commands/dump.rs
Normal file
15
crates/miroir-ctl/src/commands/dump.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use clap::Subcommand;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum DumpSubcommand {
|
||||
/// Dump data for a key or prefix
|
||||
Keys {
|
||||
/// Key or prefix to dump
|
||||
#[arg(short, long)]
|
||||
prefix: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn run(_cmd: DumpSubcommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Err("This command is not yet implemented. See bead miroir-qon for tracking.".into())
|
||||
}
|
||||
14
crates/miroir-ctl/src/commands/explain.rs
Normal file
14
crates/miroir-ctl/src/commands/explain.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
use clap::Subcommand;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum ExplainSubcommand {
|
||||
/// Explain a query plan or operation
|
||||
Query {
|
||||
/// Query or operation to explain
|
||||
query: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn run(_cmd: ExplainSubcommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Err("This command is not yet implemented. See bead miroir-qon for tracking.".into())
|
||||
}
|
||||
15
crates/miroir-ctl/src/commands/mod.rs
Normal file
15
crates/miroir-ctl/src/commands/mod.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
pub mod alias;
|
||||
pub mod canary;
|
||||
pub mod cdc;
|
||||
pub mod dump;
|
||||
pub mod explain;
|
||||
pub mod node;
|
||||
pub mod rebalance;
|
||||
pub mod reshard;
|
||||
pub mod shadow;
|
||||
pub mod status;
|
||||
pub mod task;
|
||||
pub mod tenant;
|
||||
pub mod ttl;
|
||||
pub mod ui;
|
||||
pub mod verify;
|
||||
15
crates/miroir-ctl/src/commands/node.rs
Normal file
15
crates/miroir-ctl/src/commands/node.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use clap::Subcommand;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum NodeSubcommand {
|
||||
/// Add a new node to the cluster
|
||||
Add,
|
||||
/// Remove a node from the cluster
|
||||
Remove,
|
||||
/// List all nodes
|
||||
List,
|
||||
}
|
||||
|
||||
pub async fn run(_cmd: NodeSubcommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Err("This command is not yet implemented. See bead miroir-qon for tracking.".into())
|
||||
}
|
||||
19
crates/miroir-ctl/src/commands/rebalance.rs
Normal file
19
crates/miroir-ctl/src/commands/rebalance.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
use clap::Subcommand;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum RebalanceSubcommand {
|
||||
/// Show rebalancing status
|
||||
Status {
|
||||
/// Watch mode: continuously refresh status
|
||||
#[arg(short, long)]
|
||||
watch: bool,
|
||||
},
|
||||
/// Start a rebalance operation
|
||||
Start,
|
||||
/// Cancel an active rebalance
|
||||
Cancel,
|
||||
}
|
||||
|
||||
pub async fn run(_cmd: RebalanceSubcommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Err("This command is not yet implemented. See bead miroir-qon for tracking.".into())
|
||||
}
|
||||
15
crates/miroir-ctl/src/commands/reshard.rs
Normal file
15
crates/miroir-ctl/src/commands/reshard.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use clap::Subcommand;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum ReshardSubcommand {
|
||||
/// Start a reshard operation
|
||||
Start,
|
||||
/// Show reshard status
|
||||
Status,
|
||||
/// Cancel an active reshard
|
||||
Cancel,
|
||||
}
|
||||
|
||||
pub async fn run(_cmd: ReshardSubcommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Err("This command is not yet implemented. See bead miroir-qon for tracking.".into())
|
||||
}
|
||||
17
crates/miroir-ctl/src/commands/shadow.rs
Normal file
17
crates/miroir-ctl/src/commands/shadow.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
use clap::Subcommand;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum ShadowSubcommand {
|
||||
/// Create a shadow index
|
||||
Create,
|
||||
/// Promote a shadow index to primary
|
||||
Promote,
|
||||
/// Delete a shadow index
|
||||
Delete,
|
||||
/// Show shadow index status
|
||||
Status,
|
||||
}
|
||||
|
||||
pub async fn run(_cmd: ShadowSubcommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Err("This command is not yet implemented. See bead miroir-qon for tracking.".into())
|
||||
}
|
||||
12
crates/miroir-ctl/src/commands/status.rs
Normal file
12
crates/miroir-ctl/src/commands/status.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
use clap::Parser;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct StatusSubcommand {
|
||||
/// Watch mode: continuously refresh status
|
||||
#[arg(short, long)]
|
||||
watch: bool,
|
||||
}
|
||||
|
||||
pub async fn run(_cmd: StatusSubcommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Err("This command is not yet implemented. See bead miroir-qon for tracking.".into())
|
||||
}
|
||||
22
crates/miroir-ctl/src/commands/task.rs
Normal file
22
crates/miroir-ctl/src/commands/task.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
use clap::Subcommand;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum TaskSubcommand {
|
||||
/// Show all background tasks
|
||||
List,
|
||||
/// Show task status
|
||||
Status {
|
||||
/// Task ID
|
||||
#[arg(short, long)]
|
||||
id: Option<String>,
|
||||
},
|
||||
/// Cancel a task
|
||||
Cancel {
|
||||
/// Task ID
|
||||
id: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn run(_cmd: TaskSubcommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Err("This command is not yet implemented. See bead miroir-qon for tracking.".into())
|
||||
}
|
||||
17
crates/miroir-ctl/src/commands/tenant.rs
Normal file
17
crates/miroir-ctl/src/commands/tenant.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
use clap::Subcommand;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum TenantSubcommand {
|
||||
/// Create a new tenant
|
||||
Create,
|
||||
/// List all tenants
|
||||
List,
|
||||
/// Delete a tenant
|
||||
Delete,
|
||||
/// Set tenant quota
|
||||
SetQuota,
|
||||
}
|
||||
|
||||
pub async fn run(_cmd: TenantSubcommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Err("This command is not yet implemented. See bead miroir-qon for tracking.".into())
|
||||
}
|
||||
15
crates/miroir-ctl/src/commands/ttl.rs
Normal file
15
crates/miroir-ctl/src/commands/ttl.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use clap::Subcommand;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum TtlSubcommand {
|
||||
/// Set a TTL policy
|
||||
Set,
|
||||
/// Get TTL policy for a key
|
||||
Get,
|
||||
/// Remove a TTL policy
|
||||
Remove,
|
||||
}
|
||||
|
||||
pub async fn run(_cmd: TtlSubcommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Err("This command is not yet implemented. See bead miroir-qon for tracking.".into())
|
||||
}
|
||||
15
crates/miroir-ctl/src/commands/ui.rs
Normal file
15
crates/miroir-ctl/src/commands/ui.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use clap::Subcommand;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum UiSubcommand {
|
||||
/// Launch the web UI
|
||||
Launch {
|
||||
/// Port to listen on
|
||||
#[arg(short, long, default_value = "3000")]
|
||||
port: u16,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn run(_cmd: UiSubcommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Err("This command is not yet implemented. See bead miroir-qon for tracking.".into())
|
||||
}
|
||||
15
crates/miroir-ctl/src/commands/verify.rs
Normal file
15
crates/miroir-ctl/src/commands/verify.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use clap::Subcommand;
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
pub enum VerifySubcommand {
|
||||
/// Verify data integrity for a key prefix
|
||||
Check {
|
||||
/// Key prefix to verify
|
||||
#[arg(short, long)]
|
||||
prefix: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn run(_cmd: VerifySubcommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
Err("This command is not yet implemented. See bead miroir-qon for tracking.".into())
|
||||
}
|
||||
258
crates/miroir-ctl/src/credentials.rs
Normal file
258
crates/miroir-ctl/src/credentials.rs
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
//! Admin API key credential loading.
|
||||
//!
|
||||
//! Priority order per plan §9:
|
||||
//! 1. `MIROIR_ADMIN_API_KEY` environment variable
|
||||
//! 2. `~/.config/miroir/credentials` TOML file
|
||||
//! 3. `--admin-key` CLI flag (WARNING: visible in process list!)
|
||||
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const ENV_VAR: &str = "MIROIR_ADMIN_API_KEY";
|
||||
const CREDENTIALS_FILE: &str = "credentials";
|
||||
|
||||
/// Credentials loaded from `~/.config/miroir/credentials.toml`
|
||||
#[derive(Deserialize)]
|
||||
struct CredentialsFile {
|
||||
default: Option<CredentialsProfile>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CredentialsProfile {
|
||||
admin_api_key: Option<String>,
|
||||
}
|
||||
|
||||
/// Error types for credential loading
|
||||
#[derive(Debug)]
|
||||
pub enum CredentialError {
|
||||
NotFound(String),
|
||||
IoError(std::io::Error),
|
||||
ParseError(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CredentialError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
CredentialError::NotFound(msg) => write!(f, "Credential not found: {}", msg),
|
||||
CredentialError::IoError(e) => write!(f, "IO error: {}", e),
|
||||
CredentialError::ParseError(msg) => write!(f, "Parse error: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CredentialError {}
|
||||
|
||||
/// Load admin API key from the first available source.
|
||||
///
|
||||
/// Priority order:
|
||||
/// 1. `MIROIR_ADMIN_API_KEY` environment variable
|
||||
/// 2. `~/.config/miroir/credentials` TOML file (`default.admin_api_key`)
|
||||
/// 3. Flag value (passed separately by caller)
|
||||
///
|
||||
/// Returns `Ok(Some(key))` if found, `Ok(None)` if no source has a key.
|
||||
pub fn load_admin_key(flag_key: Option<String>) -> Result<Option<String>, CredentialError> {
|
||||
// Priority 1: Environment variable
|
||||
if let Ok(key) = std::env::var(ENV_VAR) {
|
||||
if !key.is_empty() {
|
||||
return Ok(Some(key));
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: Credentials file
|
||||
if let Some(key) = load_from_credentials_file()? {
|
||||
return Ok(Some(key));
|
||||
}
|
||||
|
||||
// Priority 3: CLI flag
|
||||
if let Some(key) = flag_key {
|
||||
if !key.is_empty() {
|
||||
return Ok(Some(key));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Load admin key from `~/.config/miroir/credentials` TOML file.
|
||||
fn load_from_credentials_file() -> Result<Option<String>, CredentialError> {
|
||||
let config_dir = dirs::config_dir().ok_or_else(|| {
|
||||
CredentialError::NotFound("Unable to determine config directory".to_string())
|
||||
})?;
|
||||
|
||||
let miroir_config_dir = config_dir.join("miroir");
|
||||
let credentials_path = miroir_config_dir.join(CREDENTIALS_FILE);
|
||||
|
||||
// Try .toml extension first, then plain name
|
||||
let paths = [
|
||||
credentials_path.with_extension("toml"),
|
||||
credentials_path.clone(),
|
||||
];
|
||||
|
||||
for path in &paths {
|
||||
if path.exists() {
|
||||
let contents = fs::read_to_string(path).map_err(CredentialError::IoError)?;
|
||||
|
||||
let creds: CredentialsFile = toml::from_str(&contents)
|
||||
.map_err(|e| CredentialError::ParseError(format!("Invalid TOML: {}", e)))?;
|
||||
|
||||
if let Some(profile) = creds.default {
|
||||
if let Some(key) = profile.admin_api_key {
|
||||
if !key.is_empty() {
|
||||
return Ok(Some(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// File exists but no key found
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Get the credentials file path for diagnostic messages
|
||||
pub fn credentials_file_path() -> Option<PathBuf> {
|
||||
let config_dir = dirs::config_dir()?;
|
||||
let miroir_config_dir = config_dir.join("miroir");
|
||||
Some(
|
||||
miroir_config_dir
|
||||
.join(CREDENTIALS_FILE)
|
||||
.with_extension("toml"),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
|
||||
// Test isolation: each test gets its own temp config dir
|
||||
fn setup_test_config_dir(_test_name: &str) -> tempfile::TempDir {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let miroir_dir = temp_dir.path().join("miroir");
|
||||
fs::create_dir_all(&miroir_dir).unwrap();
|
||||
temp_dir
|
||||
}
|
||||
|
||||
fn write_credentials_file(dir: &std::path::Path, content: &str) {
|
||||
let creds_path = dir.join("credentials.toml");
|
||||
fs::write(creds_path, content).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_env_var_takes_precedence() {
|
||||
// Create a credentials file with a key
|
||||
let temp_dir = setup_test_config_dir("env_precedence");
|
||||
write_credentials_file(
|
||||
&temp_dir.path().join("miroir"),
|
||||
r#"
|
||||
[default]
|
||||
admin_api_key = "file-key-12345"
|
||||
"#,
|
||||
);
|
||||
|
||||
// Mock env var (note: this affects the actual environment for the test process)
|
||||
env::set_var(ENV_VAR, "env-key-67890");
|
||||
|
||||
// Env var should win even though file exists and flag is provided
|
||||
let result = load_admin_key(Some("flag-key-abcde".to_string())).unwrap();
|
||||
assert_eq!(result, Some("env-key-67890".to_string()));
|
||||
|
||||
env::remove_var(ENV_VAR);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_credentials_file_without_env() {
|
||||
let temp_dir = setup_test_config_dir("file_only");
|
||||
write_credentials_file(
|
||||
&temp_dir.path().join("miroir"),
|
||||
r#"
|
||||
[default]
|
||||
admin_api_key = "file-key-12345"
|
||||
"#,
|
||||
);
|
||||
|
||||
// Since we can't mock dirs::config_dir(), we'll test the parsing logic directly
|
||||
let content = r#"
|
||||
[default]
|
||||
admin_api_key = "file-key-12345"
|
||||
"#;
|
||||
let creds: CredentialsFile = toml::from_str(content).unwrap();
|
||||
assert_eq!(
|
||||
creds.default.unwrap().admin_api_key.unwrap(),
|
||||
"file-key-12345"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flag_as_fallback() {
|
||||
// No env var, no file - flag should be used
|
||||
env::remove_var(ENV_VAR);
|
||||
let result = load_admin_key(Some("flag-key-xyz".to_string())).unwrap();
|
||||
assert_eq!(result, Some("flag-key-xyz".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_credentials_returns_none() {
|
||||
env::remove_var(ENV_VAR);
|
||||
let result = load_admin_key(None as Option<String>).unwrap();
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_key_is_ignored() {
|
||||
env::set_var(ENV_VAR, "");
|
||||
let result = load_admin_key(Some("flag-key".to_string())).unwrap();
|
||||
// Empty env var should be skipped, flag key used
|
||||
assert_eq!(result, Some("flag-key".to_string()));
|
||||
env::remove_var(ENV_VAR);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_credentials_file_parsing() {
|
||||
// Test various TOML formats
|
||||
let valid_cases = vec![
|
||||
(
|
||||
r#"[default]
|
||||
admin_api_key = "key123""#,
|
||||
"key123",
|
||||
),
|
||||
(
|
||||
r#"[default]
|
||||
admin_api_key = "key456"
|
||||
"#,
|
||||
"key456",
|
||||
),
|
||||
(
|
||||
r#"[default]
|
||||
admin_api_key = "key789"
|
||||
[other]
|
||||
admin_api_key = "other-key""#,
|
||||
"key789",
|
||||
),
|
||||
];
|
||||
|
||||
for (toml, expected_key) in valid_cases {
|
||||
let creds: CredentialsFile = toml::from_str(toml).unwrap();
|
||||
assert_eq!(creds.default.unwrap().admin_api_key.unwrap(), expected_key);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_credentials_file_missing_default_section() {
|
||||
let toml = r#"[other]
|
||||
admin_api_key = "other-key""#;
|
||||
let creds: CredentialsFile = toml::from_str(toml).unwrap();
|
||||
assert!(creds.default.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_credentials_file_missing_key() {
|
||||
let toml = r#"[default]"#;
|
||||
let creds: CredentialsFile = toml::from_str(toml).unwrap();
|
||||
assert!(creds.default.unwrap().admin_api_key.is_none());
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1,3 @@
|
|||
// miroir-ctl placeholder
|
||||
//! miroir-ctl: Miroir management CLI library
|
||||
|
||||
pub mod credentials;
|
||||
|
|
|
|||
129
crates/miroir-ctl/src/main.rs
Normal file
129
crates/miroir-ctl/src/main.rs
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
use credentials::load_admin_key;
|
||||
|
||||
mod commands;
|
||||
mod credentials;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "miroir-ctl")]
|
||||
#[command(about = "Miroir management CLI", long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
|
||||
/// Admin API key for authentication.
|
||||
///
|
||||
/// WARNING: This flag's value is visible in your shell history and the process list
|
||||
/// (ps, top, etc.). For production use, prefer the MIROIR_ADMIN_API_KEY environment
|
||||
/// variable or ~/.config/miroir/credentials file.
|
||||
#[arg(long, global = true)]
|
||||
admin_key: Option<String>,
|
||||
|
||||
/// API endpoint URL (default: http://localhost:8080)
|
||||
#[arg(long, global = true)]
|
||||
api_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Commands {
|
||||
/// Show cluster status and health
|
||||
Status(commands::status::StatusSubcommand),
|
||||
|
||||
/// Manage cluster nodes
|
||||
#[command(subcommand)]
|
||||
Node(commands::node::NodeSubcommand),
|
||||
|
||||
/// Manage rebalancing operations
|
||||
#[command(subcommand)]
|
||||
Rebalance(commands::rebalance::RebalanceSubcommand),
|
||||
|
||||
/// Manage resharding operations
|
||||
#[command(subcommand)]
|
||||
Reshard(commands::reshard::ReshardSubcommand),
|
||||
|
||||
/// Verify data integrity
|
||||
#[command(subcommand)]
|
||||
Verify(commands::verify::VerifySubcommand),
|
||||
|
||||
/// Monitor and manage background tasks
|
||||
#[command(subcommand)]
|
||||
Task(commands::task::TaskSubcommand),
|
||||
|
||||
/// Dump and inspect data
|
||||
#[command(subcommand)]
|
||||
Dump(commands::dump::DumpSubcommand),
|
||||
|
||||
/// Manage key aliases
|
||||
#[command(subcommand)]
|
||||
Alias(commands::alias::AliasSubcommand),
|
||||
|
||||
/// Manage canary deployments
|
||||
#[command(subcommand)]
|
||||
Canary(commands::canary::CanarySubcommand),
|
||||
|
||||
/// Manage TTL policies
|
||||
#[command(subcommand)]
|
||||
Ttl(commands::ttl::TtlSubcommand),
|
||||
|
||||
/// Manage change data capture
|
||||
#[command(subcommand)]
|
||||
Cdc(commands::cdc::CdcSubcommand),
|
||||
|
||||
/// Manage shadow indexing
|
||||
#[command(subcommand)]
|
||||
Shadow(commands::shadow::ShadowSubcommand),
|
||||
|
||||
/// Launch the web UI
|
||||
#[command(subcommand)]
|
||||
Ui(commands::ui::UiSubcommand),
|
||||
|
||||
/// Manage multi-tenancy
|
||||
#[command(subcommand)]
|
||||
Tenant(commands::tenant::TenantSubcommand),
|
||||
|
||||
/// Explain query plans and operations
|
||||
#[command(subcommand)]
|
||||
Explain(commands::explain::ExplainSubcommand),
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Load admin API key following priority order:
|
||||
// 1. MIROIR_ADMIN_API_KEY env var
|
||||
// 2. ~/.config/miroir/credentials
|
||||
// 3. --admin-key flag
|
||||
let admin_key =
|
||||
load_admin_key(cli.admin_key).map_err(|e| format!("Failed to load credentials: {}", e))?;
|
||||
|
||||
if admin_key.is_none() {
|
||||
eprintln!("Error: No admin API key found.");
|
||||
eprintln!("Set one of:");
|
||||
eprintln!(" 1. MIROIR_ADMIN_API_KEY environment variable");
|
||||
eprintln!(" 2. ~/.config/miroir/credentials file with [default].admin_api_key");
|
||||
eprintln!(" 3. --admin-key flag (WARNING: visible in process list)");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// TODO: Use admin_key for API authentication when commands are implemented
|
||||
let _admin_key = admin_key.unwrap();
|
||||
|
||||
match cli.command {
|
||||
Commands::Status(cmd) => commands::status::run(cmd).await,
|
||||
Commands::Node(cmd) => commands::node::run(cmd).await,
|
||||
Commands::Rebalance(cmd) => commands::rebalance::run(cmd).await,
|
||||
Commands::Reshard(cmd) => commands::reshard::run(cmd).await,
|
||||
Commands::Verify(cmd) => commands::verify::run(cmd).await,
|
||||
Commands::Task(cmd) => commands::task::run(cmd).await,
|
||||
Commands::Dump(cmd) => commands::dump::run(cmd).await,
|
||||
Commands::Alias(cmd) => commands::alias::run(cmd).await,
|
||||
Commands::Canary(cmd) => commands::canary::run(cmd).await,
|
||||
Commands::Ttl(cmd) => commands::ttl::run(cmd).await,
|
||||
Commands::Cdc(cmd) => commands::cdc::run(cmd).await,
|
||||
Commands::Shadow(cmd) => commands::shadow::run(cmd).await,
|
||||
Commands::Ui(cmd) => commands::ui::run(cmd).await,
|
||||
Commands::Tenant(cmd) => commands::tenant::run(cmd).await,
|
||||
Commands::Explain(cmd) => commands::explain::run(cmd).await,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue