1pub mod client;
22pub mod cmd;
23pub mod config;
24pub mod exit;
25pub mod model;
26pub mod output;
27
28use std::path::PathBuf;
29
30use clap::{Parser, Subcommand};
31
32use crate::client::CellosClient;
33use crate::exit::{CtlError, CtlResult};
34use crate::output::OutputFormat;
35
36#[derive(Parser, Debug)]
38#[command(
39 name = "cellctl",
40 version,
41 about = "kubectl-style CLI for CellOS",
42 long_about = None,
43)]
44struct Cli {
45 #[arg(long, global = true, env = "CELLCTL_SERVER")]
47 server: Option<String>,
48
49 #[arg(long, global = true, env = "CELLCTL_TOKEN", hide_env_values = true)]
51 token: Option<String>,
52
53 #[command(subcommand)]
54 cmd: Cmd,
55}
56
57#[derive(Subcommand, Debug)]
58enum Cmd {
59 Apply {
61 #[arg(short = 'f', long = "file")]
63 file: PathBuf,
64 },
65 Get {
67 #[command(subcommand)]
68 what: GetWhat,
69 },
70 Describe {
72 #[command(subcommand)]
73 what: DescribeWhat,
74 },
75 Delete {
77 #[command(subcommand)]
78 what: DeleteWhat,
79 },
80 Logs {
82 cell: String,
84 #[arg(long, short = 'f')]
86 follow: bool,
87 #[arg(long)]
89 tail: Option<usize>,
90 },
91 Events {
93 #[arg(long)]
95 formation: Option<String>,
96 #[arg(long, short = 'f')]
98 follow: bool,
99 },
100 Rollout {
102 #[command(subcommand)]
103 what: RolloutWhat,
104 },
105 Diff {
107 #[arg(short = 'f', long = "file")]
109 file: PathBuf,
110 },
111 Config {
113 #[command(subcommand)]
114 what: ConfigWhat,
115 },
116 Version,
118 Webui {
120 #[arg(long)]
122 open: bool,
123 #[arg(long, value_enum, default_value = "auto")]
126 bind: cmd::webui::BindMode,
127 },
128}
129
130#[derive(Subcommand, Debug)]
131enum GetWhat {
132 Formations {
134 #[arg(long, short = 'o', default_value = "table")]
135 output: String,
136 },
137 Cells {
139 #[arg(long)]
140 formation: Option<String>,
141 #[arg(long, short = 'o', default_value = "table")]
142 output: String,
143 },
144}
145
146#[derive(Subcommand, Debug)]
147enum DescribeWhat {
148 Formation {
150 name: String,
152 },
153 Cell {
155 name: String,
157 },
158}
159
160#[derive(Subcommand, Debug)]
161enum DeleteWhat {
162 Formation {
164 name: String,
166 #[arg(long, short = 'y')]
168 yes: bool,
169 },
170}
171
172#[derive(Subcommand, Debug)]
173enum RolloutWhat {
174 Status {
176 name: String,
178 #[arg(long)]
180 timeout: Option<u64>,
181 },
182}
183
184#[derive(Subcommand, Debug)]
185enum ConfigWhat {
186 SetServer {
188 url: String,
190 },
191 SetToken {
193 token: String,
195 },
196 Show,
198}
199
200pub fn run() {
205 if std::env::var_os("RUST_LOG").is_some() {
214 use tracing_subscriber::layer::SubscriberExt;
215 use tracing_subscriber::util::SubscriberInitExt;
216 use tracing_subscriber::Layer;
217
218 let fmt_layer = tracing_subscriber::fmt::layer()
219 .with_writer(std::io::stderr)
220 .with_filter(cellos_core::observability::redacted_filter());
221
222 let _ = tracing_subscriber::registry()
223 .with(tracing_subscriber::EnvFilter::from_default_env())
224 .with(fmt_layer)
225 .try_init();
226 }
227
228 let cli = Cli::parse();
229
230 let rt = match tokio::runtime::Builder::new_current_thread()
233 .enable_all()
234 .build()
235 {
236 Ok(rt) => rt,
237 Err(e) => CtlError::usage(format!("init tokio runtime: {e}")).exit(),
238 };
239
240 match rt.block_on(dispatch(cli)) {
241 Ok(()) => {}
242 Err(e) => e.exit(),
243 }
244}
245
246async fn dispatch(cli: Cli) -> CtlResult<()> {
247 let mut cfg = config::load().unwrap_or_default();
249 if let Some(s) = cli.server {
250 cfg.server_url = Some(s);
251 }
252 if let Some(t) = cli.token {
253 cfg.api_token = Some(t);
254 }
255
256 match cli.cmd {
259 Cmd::Config { what } => return run_config(what),
260 Cmd::Version => {
261 let client = CellosClient::new(&cfg)?;
262 return cmd::version::run(&client).await;
263 }
264 Cmd::Webui { open, bind } => {
265 return cmd::webui::run(&cfg, open, bind).await;
266 }
267 _ => {}
268 }
269
270 let client = CellosClient::new(&cfg)?;
271
272 match cli.cmd {
273 Cmd::Apply { file } => cmd::apply::run(&client, &file).await,
274 Cmd::Get { what } => match what {
275 GetWhat::Formations { output } => {
276 let fmt: OutputFormat = output.parse()?;
277 cmd::get::formations(&client, fmt).await
278 }
279 GetWhat::Cells { formation, output } => {
280 let fmt: OutputFormat = output.parse()?;
281 cmd::get::cells(&client, formation.as_deref(), fmt).await
282 }
283 },
284 Cmd::Describe { what } => match what {
285 DescribeWhat::Formation { name } => cmd::describe::formation(&client, &name).await,
286 DescribeWhat::Cell { name } => cmd::describe::cell(&client, &name).await,
287 },
288 Cmd::Delete { what } => match what {
289 DeleteWhat::Formation { name, yes } => {
290 cmd::delete::formation(&client, &name, yes).await
291 }
292 },
293 Cmd::Logs { cell, follow, tail } => cmd::logs::run(&client, &cell, follow, tail).await,
294 Cmd::Events { formation, follow } => {
295 cmd::events::run(&client, formation.as_deref(), follow).await
296 }
297 Cmd::Rollout { what } => match what {
298 RolloutWhat::Status { name, timeout } => {
299 cmd::rollout::status(&client, &name, timeout).await
300 }
301 },
302 Cmd::Diff { file } => cmd::diff::run(&client, &file).await,
303 Cmd::Config { .. } | Cmd::Version | Cmd::Webui { .. } => {
304 unreachable!("handled above")
305 }
306 }
307}
308
309fn run_config(what: ConfigWhat) -> CtlResult<()> {
310 match what {
311 ConfigWhat::SetServer { url } => cmd::config_cmd::set_server(&url),
312 ConfigWhat::SetToken { token } => cmd::config_cmd::set_token(&token),
313 ConfigWhat::Show => cmd::config_cmd::show(),
314 }
315}