xianyun_cli/commands/
deploy.rs1use std::sync::Arc;
6
7use anyhow::Context as _;
8use clap::Args;
9use xianyun_cluster::clusterdomain;
10use xianyun_cluster::driver::{Driver, State};
11use xianyun_core::config::caproni::load_from_context;
12use xianyun_core::config::global::Context;
13use xianyun_core::config::latest::CaproniConfig;
14use xianyun_deploy::deploy::orchestrator::{DeployerFilterFunc, Orchestrator};
15use xianyun_executor::exec::CommandRunner;
16
17#[derive(Args, Debug)]
18pub struct DeployArgs {
19 #[arg(long = "repo")]
22 pub repo: Option<String>,
23
24 #[arg(long = "update")]
27 pub update: bool,
28}
29
30pub fn run(args: &DeployArgs, ctx: &Context) -> anyhow::Result<()> {
31 run_deploy(
32 ctx,
33 args.repo.as_deref(),
34 args.update,
35 crate::commands::up::connect_kubectl,
36 )
37}
38
39#[derive(Debug, thiserror::Error)]
46pub enum DeployError {
47 #[error("cluster is not ready (state: {0:?}). Run 'caproni up' first")]
48 ClusterNotReady(Option<State>),
49 #[error("one or more repository updates failed")]
50 RepositoryUpdatesFailed,
51}
52
53fn verify_cluster_ready(mgr: &dyn Driver) -> anyhow::Result<()> {
60 let status = mgr.status().context("checking cluster status")?;
61
62 if status.state != Some(State::Ready) {
63 anyhow::bail!(DeployError::ClusterNotReady(status.state));
64 }
65
66 tracing::info!(state = ?status.state, driver = mgr.name(), "cluster verified");
67
68 Ok(())
69}
70
71pub fn run_deploy(
84 ctx: &Context,
85 repo_filter: Option<&str>,
86 update: bool,
87 connect: clusterdomain::resolve::Connector,
88) -> anyhow::Result<()> {
89 crate::drivers::ensure_registered();
90
91 let mut container = load_from_context(ctx).context("loading configuration")?;
92
93 let deployer_filter: Option<Box<DeployerFilterFunc<'_>>> = repo_filter.map(|repo| {
94 tracing::info!(deployer = repo, "deploying single deployer");
95 Box::new(move |name: &str| name == repo) as Box<DeployerFilterFunc<'_>>
96 });
97
98 let manager = xianyun_cluster::registry::new_manager(
99 container.caproni.cluster.clone(),
100 container.global.profile_name.clone(),
101 )
102 .context("creating cluster manager")?;
103
104 verify_cluster_ready(manager.as_ref()).context("verifying cluster ready")?;
105
106 let kubeconfig = manager.get_kubeconfig().context("getting kubeconfig")?;
107
108 let reader = connect(manager.as_ref()).context("connecting to cluster DNS")?;
109
110 let mut container = clusterdomain::resolve::resolve_and_reload(
111 &mut container,
112 Some(manager.as_ref()),
113 reader.as_ref(),
114 )
115 .context("resolving cluster DNS domain")?;
116
117 if update {
118 tracing::info!("updating repositories");
119
120 if let Err(e) = update_repositories_for_deploy(&container, deployer_filter.as_deref()) {
121 tracing::warn!(error = %e, "repository update encountered errors");
122 }
123 }
124
125 tracing::info!("deploying charts");
126
127 let orch = Orchestrator::new();
128 orch.deploy_all(&container, deployer_filter.as_deref(), &kubeconfig)
129 .context("deploying charts")?;
130
131 tracing::info!("all deployments complete");
132
133 let _ = &mut container;
134
135 Ok(())
136}
137
138fn update_repositories_for_deploy(
143 container: &xianyun_core::config::container::Container,
144 deployer_filter: Option<&DeployerFilterFunc<'_>>,
145) -> Result<(), DeployError> {
146 let executor: Arc<dyn CommandRunner + Send + Sync> =
147 Arc::new(xianyun_executor::exec::Client::new());
148 let git_backend = Arc::new(xianyun_backends::git::Client::new(executor.clone()));
149
150 let repo_filter = build_repo_filter_for_deployers(&container.caproni, deployer_filter);
151
152 let updater = xianyun_update::updater::Updater::new(
153 git_backend,
154 xianyun_update::updater::UpdateOptions {
155 strategy: xianyun_update::updater::UpdateStrategy::FullUpdate,
156 filter: Some(repo_filter),
157 ..Default::default()
158 },
159 );
160
161 let (results, success) = updater.update_repositories(executor.as_ref(), container);
162
163 for result in &results {
164 match result.status {
165 xianyun_update::updater::UpdateStatus::Failed => {
166 tracing::warn!(
167 repo = %result.repo_name,
168 error = ?result.error,
169 "repository update failed"
170 );
171 }
172 xianyun_update::updater::UpdateStatus::Updated => {
173 tracing::info!(repo = %result.repo_name, notes = %result.notes, "repository updated");
174 }
175 _ => {}
176 }
177 }
178
179 if !success {
180 return Err(DeployError::RepositoryUpdatesFailed);
181 }
182
183 Ok(())
184}
185
186fn build_repo_filter_for_deployers(
190 caproni_cfg: &CaproniConfig,
191 deployer_filter: Option<&DeployerFilterFunc<'_>>,
192) -> xianyun_update::updater::RepoFilterFn {
193 let mut required_repos = std::collections::HashSet::new();
198
199 if let Some(deployers) = &caproni_cfg.deployers {
200 for (name, deployer_config) in deployers.iter() {
201 if let Some(filter) = deployer_filter
202 && !filter(name)
203 {
204 continue;
205 }
206
207 if let Some(repository) = &deployer_config.repository {
208 required_repos.insert(repository.clone());
209 }
210 }
211 }
212
213 Box::new(move |repo_name: &str| required_repos.contains(repo_name))
214}