xianyun_cli/commands/
ca.rs1use std::path::Path;
6use std::sync::Arc;
7
8use anyhow::Context as _;
9use clap::{Args, Subcommand};
10use xianyun_cluster::clusterdomain;
11use xianyun_core::config::caproni::load_from_context;
12use xianyun_core::config::container::Container;
13use xianyun_core::config::global::Context;
14use xianyun_executor::exec::CommandRunner as ExecCommandRunner;
15use xianyun_pki::truststore::command_runner::{CmdError, CommandRunner as TruststoreCommandRunner};
16
17#[derive(Args, Debug)]
18pub struct CaArgs {
19 #[command(subcommand)]
20 pub command: CaCommands,
21}
22
23#[derive(Subcommand, Debug)]
24pub enum CaCommands {
25 Trust(TrustArgs),
27
28 Untrust(UntrustArgs),
30
31 Inspect(InspectArgs),
33}
34
35#[derive(Args, Debug)]
36pub struct TrustArgs {}
37
38#[derive(Args, Debug)]
39pub struct UntrustArgs {}
40
41#[derive(Args, Debug)]
42pub struct InspectArgs {}
43
44pub fn run(args: &CaArgs, ctx: &Context) -> anyhow::Result<()> {
45 match &args.command {
46 CaCommands::Trust(_) => run_trust(ctx),
47 CaCommands::Untrust(_) => run_untrust(ctx),
48 CaCommands::Inspect(_) => run_inspect(ctx),
49 }
50}
51
52struct ExecTruststoreRunner {
57 exec: Arc<dyn ExecCommandRunner + Send + Sync>,
58}
59
60impl TruststoreCommandRunner for ExecTruststoreRunner {
61 fn run_sudo(&self, name: &str, args: &[String]) -> Result<(), CmdError> {
62 self.exec
63 .run_sudo(name, args)
64 .map_err(|e| CmdError(e.to_string()))
65 }
66
67 fn read_stdout(&self, name: &str, args: &[String]) -> Result<String, CmdError> {
68 self.exec
69 .read_stdout(name, args)
70 .map_err(|e| CmdError(e.to_string()))
71 }
72}
73
74fn fmt_go_slice(items: &[String]) -> String {
81 format!("[{}]", items.join(" "))
82}
83
84fn permitted_dns_from_config(container: &Container) -> Vec<String> {
86 crate::commands::up::permitted_dns_for_cluster(
87 &container.caproni.cluster,
88 &container.global.profile_name,
89 )
90}
91
92fn configured_domain(container: &Container) -> String {
94 container
95 .caproni
96 .cluster
97 .network
98 .as_ref()
99 .map(|n| n.cluster_domain.clone())
100 .unwrap_or_default()
101}
102
103fn domain_line(
107 container: &Container,
108 skipped: Option<&clusterdomain::resolve::ResolveError>,
109) -> String {
110 let domain = configured_domain(container);
111 let source = &container.global.cluster_domain_source;
112
113 if !source.is_empty() && source != clusterdomain::resolve::CONFIGURED_SOURCE {
114 format!("{domain} (read from {source})")
115 } else if let Some(skipped) = skipped {
116 format!(
117 "{domain} ({}; {skipped})",
118 clusterdomain::resolve::CONFIGURED_SOURCE
119 )
120 } else {
121 format!("{domain} ({})", clusterdomain::resolve::CONFIGURED_SOURCE)
122 }
123}
124
125pub fn load_config(
138 ctx: &Context,
139 connect: clusterdomain::resolve::Connector,
140) -> anyhow::Result<(Container, Option<clusterdomain::resolve::ResolveError>)> {
141 crate::drivers::ensure_registered();
142
143 let mut container = load_from_context(ctx).context("loading caproni config")?;
144
145 let manager = xianyun_cluster::registry::new_manager(
146 container.caproni.cluster.clone(),
147 container.global.profile_name.clone(),
148 )
149 .context("creating cluster manager")?;
150
151 let (skipped, err) =
152 clusterdomain::resolve::resolve_running(&mut container, manager.as_ref(), connect);
153
154 if let Some(err) = err {
155 return Err(anyhow::Error::new(err).context("resolving cluster DNS domain"));
156 }
157
158 Ok((container, skipped))
159}
160
161fn run_trust(ctx: &Context) -> anyhow::Result<()> {
163 let (container, _skipped) = load_config(ctx, crate::commands::up::connect_kubectl)?;
164
165 let pki_dir = crate::commands::up::pki_dir_for_profile(&container.global.profile_name)
166 .context("resolving PKI directory")?;
167
168 ensure_trusted_ca(&container, &pki_dir)?;
169
170 let runner = ExecTruststoreRunner {
171 exec: Arc::new(xianyun_executor::exec::Client::new()),
172 };
173
174 xianyun_pki::truststore::install(&pki_dir, &runner).context("installing CA")?;
175
176 Ok(())
177}
178
179fn ensure_trusted_ca(container: &Container, pki_dir: &Path) -> anyhow::Result<()> {
182 let constraints = permitted_dns_from_config(container);
183
184 let (_, previous) =
185 xianyun_pki::ca::ensure_current(pki_dir, &constraints).context("loading CA")?;
186
187 if let Some(previous) = previous {
188 println!("CA constraints changed, re-signed the Caproni Dev CA with its existing key.");
189 println!(" Was: {}", fmt_go_slice(&previous));
190 println!(" Now: {}", fmt_go_slice(&constraints));
191 }
192
193 Ok(())
194}
195
196fn run_untrust(ctx: &Context) -> anyhow::Result<()> {
198 crate::drivers::ensure_registered();
199
200 let container = load_from_context(ctx).context("loading caproni config")?;
201
202 let pki_dir = crate::commands::up::pki_dir_for_profile(&container.global.profile_name)
203 .context("resolving PKI directory")?;
204
205 let runner = ExecTruststoreRunner {
206 exec: Arc::new(xianyun_executor::exec::Client::new()),
207 };
208
209 xianyun_pki::truststore::uninstall(&pki_dir, &runner).context("uninstalling CA")?;
210
211 Ok(())
212}
213
214pub fn run_inspect(ctx: &Context) -> anyhow::Result<()> {
217 run_inspect_with_connector(ctx, crate::commands::up::connect_kubectl)
218}
219
220pub fn run_inspect_with_connector(
224 ctx: &Context,
225 connect: clusterdomain::resolve::Connector,
226) -> anyhow::Result<()> {
227 let (container, skipped) = load_config(ctx, connect)?;
228
229 let pki_dir = crate::commands::up::pki_dir_for_profile(&container.global.profile_name)
230 .context("resolving PKI directory")?;
231
232 let constraints = permitted_dns_from_config(&container);
233
234 let ca = xianyun_pki::ca::load_or_generate(&pki_dir, &constraints).context("loading CA")?;
235
236 let cert_path = xianyun_pki::ca::cert_path(&pki_dir).context("resolving CA certificate")?;
237
238 println!("Caproni Dev CA");
239 println!(" Path: {}", cert_path.display());
240 println!(" Subject: {}", ca.cert.common_name);
241 println!(" Not Before: {:?}", ca.cert.not_before);
242 println!(" Not After: {:?}", ca.cert.not_after);
243 println!(
244 " Domain: {}",
245 domain_line(&container, skipped.as_ref())
246 );
247 println!(
248 " Constraints: {}",
249 fmt_go_slice(&ca.cert.permitted_dns_domains)
250 );
251
252 if !xianyun_pki::ca::constraints_equal(&ca.cert.permitted_dns_domains, &constraints) {
253 println!(
254 " Status: OUT OF DATE \u{2014} configured constraints are {}; run `caproni ca trust` to re-sign",
255 fmt_go_slice(&constraints)
256 );
257 }
258
259 Ok(())
260}