use std::path::PathBuf; use anyhow::Result; use clap::{Parser, Subcommand, ValueEnum}; use tracing_subscriber::EnvFilter; mod chainfire; mod power; mod remote; /// Deployer control CLI for PhotonCloud. /// /// - 初回ブートストラップ時に Chainfire 上の Cluster/Node/Service 定義を作成 /// - 既存の Deployer クラスタに対して宣言的な設定を apply する /// - Deployer が壊れた場合でも、Chainfire 上の状態を直接修復できることを目標とする #[derive(Parser, Debug)] #[command(author, version, about)] struct Cli { /// Chainfire API エンドポイント (例: http://127.0.0.1:7000) #[arg(long, global = true, default_value = "http://127.0.0.1:7000")] chainfire_endpoint: String, /// PhotonCloud Cluster ID (論理名) #[arg(long, global = true)] cluster_id: Option, /// PhotonCloud cluster namespace (default: photoncloud) #[arg(long, global = true, default_value = "photoncloud")] cluster_namespace: String, /// Deployer namespace used for machine_id -> NodeConfig bootstrap mappings #[arg(long, global = true, default_value = "deployer")] deployer_namespace: String, #[command(subcommand)] command: Command, } #[derive(Subcommand, Debug)] enum Command { /// 初回ブートストラップ用の基本オブジェクトを作成する /// /// - Cluster メタデータ /// - Node 情報 (ローカルノード1台分) /// - 必要であれば Service/ServiceInstance のシード Bootstrap { /// ブートストラップ用のJSON/YAML設定ファイル #[arg(long)] config: PathBuf, }, /// 宣言的な PhotonCloud クラスタ設定を Chainfire に apply する (GitOps 的に利用可能) Apply { /// Cluster/Node/Service/Instance/MTLSPolicy を含むJSON/YAML #[arg(long)] config: PathBuf, /// 既存エントリを pruning するかどうか #[arg(long, default_value_t = false)] prune: bool, }, /// Chainfire 上の PhotonCloud 関連キーをダンプする (デバッグ用途) Dump { /// ダンプ対象の prefix (未指定の場合は cluster-namespace を使用) #[arg(long, default_value = "")] prefix: String, /// 出力形式 #[arg(long, value_enum, default_value_t = DumpFormat::Text)] format: DumpFormat, }, /// Deployer HTTP API を経由して、クラスタ状態を同期・確認する /// /// 現時点ではプレースホルダであり、将来的なGitOps連携を見据えた形だけ用意する。 Deployer { /// Deployer HTTP エンドポイント (例: http://deployer.local:8080) #[arg(long)] endpoint: String, /// 一旦は `status` のみをサポート #[arg(long, default_value = "status")] action: String, }, /// ノード単位の inventory / lifecycle 状態を確認・更新する Node { #[command(subcommand)] command: NodeCommand, }, /// HostDeployment rollout object を確認・操作する Deployment { #[command(subcommand)] command: DeploymentCommand, }, /// Service spec/status/publication/instances を表示する Service { #[command(subcommand)] command: ServiceCommand, }, } #[derive(Subcommand, Debug)] enum NodeCommand { /// 指定ノードの記録と関連 state を表示する Inspect { #[arg(long)] node_id: String, #[arg(long, default_value_t = false)] include_desired_system: bool, #[arg(long, default_value_t = false)] include_observed_system: bool, #[arg(long, value_enum, default_value_t = DumpFormat::Json)] format: DumpFormat, }, /// 指定ノードの lifecycle / commissioning 状態を更新する SetState { #[arg(long)] node_id: String, #[arg(long, value_enum)] state: Option, #[arg(long, value_enum)] commission_state: Option, #[arg(long, value_enum)] install_state: Option, #[arg(long, value_enum)] power_state: Option, #[arg(long)] bmc_ref: Option, }, /// 指定ノードの observed-system を更新する SetObserved { #[arg(long)] node_id: String, #[arg(long)] status: Option, #[arg(long)] nixos_configuration: Option, #[arg(long)] target_system: Option, #[arg(long)] current_system: Option, #[arg(long)] configured_system: Option, #[arg(long)] booted_system: Option, #[arg(long)] rollback_system: Option, }, /// 指定ノードの電源操作を行う Power { #[arg(long)] node_id: String, #[arg(long, value_enum)] action: PowerActionArg, }, /// 指定ノードに再インストールを要求する Reinstall { #[arg(long)] node_id: String, #[arg(long, default_value_t = false)] power_cycle: bool, }, } #[derive(Subcommand, Debug)] enum DeploymentCommand { /// HostDeployment の spec/status を表示する Inspect { #[arg(long)] name: String, #[arg(long, value_enum, default_value_t = DumpFormat::Json)] format: DumpFormat, }, /// HostDeployment を一時停止する Pause { #[arg(long)] name: String, }, /// HostDeployment を再開する Resume { #[arg(long)] name: String, }, /// HostDeployment を中止し、配布済み desired-system を取り消す Abort { #[arg(long)] name: String, }, } #[derive(Subcommand, Debug)] enum ServiceCommand { /// Service の spec/status/publication を表示する Inspect { #[arg(long)] name: String, #[arg(long, default_value_t = false)] include_instances: bool, #[arg(long, value_enum, default_value_t = DumpFormat::Json)] format: DumpFormat, }, } #[derive(Clone, Copy, Debug, ValueEnum)] enum DumpFormat { Text, Json, } #[derive(Clone, Copy, Debug, ValueEnum)] enum NodeLifecycleStateArg { Pending, Provisioning, Active, Failed, Draining, } impl NodeLifecycleStateArg { fn as_str(self) -> &'static str { match self { Self::Pending => "pending", Self::Provisioning => "provisioning", Self::Active => "active", Self::Failed => "failed", Self::Draining => "draining", } } } #[derive(Clone, Copy, Debug, ValueEnum)] enum CommissionStateArg { Discovered, Commissioning, Commissioned, } impl CommissionStateArg { fn as_str(self) -> &'static str { match self { Self::Discovered => "discovered", Self::Commissioning => "commissioning", Self::Commissioned => "commissioned", } } } #[derive(Clone, Copy, Debug, ValueEnum)] enum InstallStateArg { Pending, Installing, Installed, Failed, ReinstallRequested, } impl InstallStateArg { fn as_str(self) -> &'static str { match self { Self::Pending => "pending", Self::Installing => "installing", Self::Installed => "installed", Self::Failed => "failed", Self::ReinstallRequested => "reinstall_requested", } } } #[derive(Clone, Copy, Debug, ValueEnum)] enum PowerStateArg { On, Off, Cycling, Unknown, } impl PowerStateArg { fn as_str(self) -> &'static str { match self { Self::On => "on", Self::Off => "off", Self::Cycling => "cycling", Self::Unknown => "unknown", } } } #[derive(Clone, Copy, Debug, ValueEnum)] enum PowerActionArg { On, Off, Cycle, Refresh, } impl PowerActionArg { fn as_str(self) -> &'static str { match self { Self::On => "on", Self::Off => "off", Self::Cycle => "cycle", Self::Refresh => "refresh", } } } #[tokio::main] async fn main() -> Result<()> { let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); tracing_subscriber::fmt() .with_writer(std::io::stderr) .with_env_filter(env_filter) .init(); let cli = Cli::parse(); match cli.command { Command::Bootstrap { config } => { chainfire::bootstrap_cluster( &cli.chainfire_endpoint, &cli.cluster_namespace, &cli.deployer_namespace, cli.cluster_id.as_deref(), &config, ) .await?; } Command::Apply { config, prune } => { chainfire::apply_cluster_state( &cli.chainfire_endpoint, &cli.cluster_namespace, &cli.deployer_namespace, cli.cluster_id.as_deref(), &config, prune, ) .await?; } Command::Dump { prefix, format } => { let resolved_prefix = if prefix.trim().is_empty() { format!("{}/", cli.cluster_namespace) } else { prefix }; chainfire::dump_prefix( &cli.chainfire_endpoint, &resolved_prefix, matches!(format, DumpFormat::Json), ) .await?; } Command::Deployer { endpoint, action } => { remote::run_deployer_command(&endpoint, &action).await?; } Command::Node { command } => { let cluster_id = cli .cluster_id .as_deref() .ok_or_else(|| anyhow::anyhow!("--cluster-id is required for node commands"))?; match command { NodeCommand::Inspect { node_id, include_desired_system, include_observed_system, format, } => { chainfire::inspect_node( &cli.chainfire_endpoint, &cli.cluster_namespace, cluster_id, &node_id, include_desired_system, include_observed_system, matches!(format, DumpFormat::Json), ) .await?; } NodeCommand::SetState { node_id, state, commission_state, install_state, power_state, bmc_ref, } => { chainfire::set_node_states( &cli.chainfire_endpoint, &cli.cluster_namespace, cluster_id, &node_id, state.map(|value| value.as_str().to_string()), commission_state.map(|value| value.as_str().to_string()), install_state.map(|value| value.as_str().to_string()), power_state.map(|value| value.as_str().to_string()), bmc_ref, ) .await?; } NodeCommand::SetObserved { node_id, status, nixos_configuration, target_system, current_system, configured_system, booted_system, rollback_system, } => { chainfire::set_observed_system( &cli.chainfire_endpoint, &cli.cluster_namespace, cluster_id, &node_id, status, nixos_configuration, target_system, current_system, configured_system, booted_system, rollback_system, ) .await?; } NodeCommand::Power { node_id, action } => { power::power_node( &cli.chainfire_endpoint, &cli.cluster_namespace, cluster_id, &node_id, action.as_str(), ) .await?; } NodeCommand::Reinstall { node_id, power_cycle, } => { power::request_reinstall( &cli.chainfire_endpoint, &cli.cluster_namespace, cluster_id, &node_id, power_cycle, ) .await?; } } } Command::Deployment { command } => { let cluster_id = cli.cluster_id.as_deref().ok_or_else(|| { anyhow::anyhow!("--cluster-id is required for deployment commands") })?; match command { DeploymentCommand::Inspect { name, format } => { chainfire::inspect_host_deployment( &cli.chainfire_endpoint, &cli.cluster_namespace, cluster_id, &name, matches!(format, DumpFormat::Json), ) .await?; } DeploymentCommand::Pause { name } => { chainfire::set_host_deployment_paused( &cli.chainfire_endpoint, &cli.cluster_namespace, cluster_id, &name, true, ) .await?; } DeploymentCommand::Resume { name } => { chainfire::set_host_deployment_paused( &cli.chainfire_endpoint, &cli.cluster_namespace, cluster_id, &name, false, ) .await?; } DeploymentCommand::Abort { name } => { chainfire::abort_host_deployment( &cli.chainfire_endpoint, &cli.cluster_namespace, cluster_id, &name, ) .await?; } } } Command::Service { command } => { let cluster_id = cli .cluster_id .as_deref() .ok_or_else(|| anyhow::anyhow!("--cluster-id is required for service commands"))?; match command { ServiceCommand::Inspect { name, include_instances, format, } => { chainfire::inspect_service( &cli.chainfire_endpoint, &cli.cluster_namespace, cluster_id, &name, include_instances, matches!(format, DumpFormat::Json), ) .await?; } } } } Ok(()) }