illa/command/
deploy.rs

1use crate::{command::*, result::Result};
2use anyhow::Ok;
3use bollard::container::{Config, CreateContainerOptions, LogsOptions, StartContainerOptions};
4use bollard::image::CreateImageOptions;
5use bollard::models::{HostConfig, Mount, MountTypeEnum};
6use bollard::service::PortBinding;
7use bollard::{service::CreateImageInfo, Docker};
8use clap::{ArgAction::SetTrue, ArgGroup, Args};
9use console::style;
10use futures_util::{StreamExt, TryStreamExt};
11use indicatif::{HumanDuration, MultiProgress, ProgressBar, ProgressStyle};
12use std::collections::HashMap;
13use std::fmt::format;
14use std::hash::Hash;
15use std::thread;
16use std::time::{Duration, Instant};
17use std::{env, process, string};
18use uuid::Uuid;
19
20const ILLA_BUILDER_IMAGE: &str = "illasoft/illa-builder";
21const ILLA_BUILDER_VERSION: &str = "latest";
22
23// Executes the `illa deploy` command to
24// deploy your ILLA Builder
25#[derive(Debug, Args)]
26#[clap(group(
27    ArgGroup::new("install")
28        .required(true)
29        .args(&["self_host", "cloud"]),
30))]
31/// Deploy the ILLA Builder
32pub struct Cmd {
33    /// Self-hosted installation
34    #[clap(short = 'S', long = "self", action = SetTrue)]
35    self_host: bool,
36
37    /// ILLA Cloud installation
38    #[clap(short = 'C', long = "cloud", action = SetTrue)]
39    cloud: bool,
40
41    /// Set the version of ILLA Builder [default: latest]
42    #[clap(short = 'V', long = "builder-version", value_name = "X.Y.Z")]
43    builder_version: Option<String>,
44
45    /// The port on which you want ILLA Builder to run
46    #[clap(short = 'p', long = "port", default_value = "80")]
47    port: u16,
48
49    /// The mount path for the ILLA Builder
50    #[clap(short = 'm', long = "mount", value_name = "/TEMP/DIR/ILLA-BUILDER")]
51    mount_path: Option<String>,
52}
53
54impl Cmd {
55    pub async fn run(&self) -> Result {
56        let spinner_style = ProgressStyle::with_template("{spinner} {wide_msg}")
57            .unwrap()
58            .tick_strings(&["🔸 ", "🔶 ", "🟠 ", "🟠 ", "🔶 "]);
59
60        let (self_host, cloud) = (self.self_host, self.cloud);
61        match (self_host, cloud) {
62            (true, _) => {
63                deploy_self_host(
64                    self.builder_version.as_ref(),
65                    self.port,
66                    self.mount_path.as_ref(),
67                    spinner_style,
68                )
69                .await?
70            }
71            (_, true) => deploy_cloud(spinner_style).await?,
72            _ => unreachable!(),
73        };
74        Ok(())
75    }
76}
77
78async fn deploy_self_host(
79    version: Option<&String>,
80    port: u16,
81    mount_path: Option<&String>,
82    progress_style: ProgressStyle,
83) -> Result {
84    println!("{} Running a self-hosted installation...", ui::emoji::BUILD);
85
86    let _docker = Docker::connect_with_local_defaults().unwrap();
87    if (_docker.ping().await).is_err() {
88        println!(
89            "{} {}\n{} {}",
90            ui::emoji::FAIL,
91            String::from("No running docker found."),
92            ui::emoji::WARN,
93            style("Please check the status of docker.").red(),
94        );
95        process::exit(1);
96    }
97
98    let m = MultiProgress::new();
99    let pb_download = m.add(ProgressBar::new(0));
100    pb_download.set_style(progress_style.clone());
101    let finish_spinner_style = ProgressStyle::with_template("{wide_msg}").unwrap();
102
103    let default_version = ILLA_BUILDER_VERSION.to_owned();
104    let builder_version = version.unwrap_or(&default_version);
105    let builder_image = ILLA_BUILDER_IMAGE.to_owned() + ":" + builder_version;
106
107    let default_mount_path = utils::get_default_mount();
108    let mount_path = mount_path.unwrap_or(&default_mount_path);
109
110    let download_started = Instant::now();
111    let stream_list = &mut _docker.create_image(
112        Some(CreateImageOptions {
113            from_image: builder_image.clone(),
114            ..Default::default()
115        }),
116        None,
117        None,
118    );
119
120    while let Some(value) = stream_list.next().await {
121        pb_download.set_message(format!("Downloading {}...", builder_image.clone()));
122        pb_download.inc(1);
123        thread::sleep(Duration::from_millis(100));
124        if value.is_err() {
125            pb_download.set_style(finish_spinner_style.clone());
126            pb_download.finish_with_message(format!(
127                "{} {} {}",
128                ui::emoji::FAIL,
129                String::from("Download image error:"),
130                style(value.err().unwrap()).red(),
131            ));
132            process::exit(1);
133        };
134    }
135    pb_download.set_style(finish_spinner_style.clone());
136    pb_download.finish_with_message(format!(
137        "{} Downloaded in {}",
138        ui::emoji::SUCCESS,
139        HumanDuration(download_started.elapsed())
140    ));
141
142    let pb_deploy = m.add(ProgressBar::new(0));
143    pb_deploy.set_style(progress_style.clone());
144
145    let pg_pwd = Uuid::new_v4();
146    let builder_env = vec![
147        "ILLA_SERVER_MODE=release".to_string(),
148        "ILLA_DEPLOY_MODE=self-host".to_string(),
149        format!("POSTGRES_PASSWORD={pg_pwd}"),
150    ];
151    let mut builder_labels = HashMap::new();
152    builder_labels.insert(
153        "maintainer".to_string(),
154        "opensource@illasoft.com".to_string(),
155    );
156    builder_labels.insert("license".to_string(), "Apache-2.0".to_string());
157    let mut builder_port_bindings = HashMap::new();
158    builder_port_bindings.insert(
159        "2022/tcp".to_string(),
160        Some(vec![PortBinding {
161            host_port: Some(port.to_string()),
162            host_ip: Some("0.0.0.0".to_string()),
163        }]),
164    );
165
166    let local_dir = utils::local_bind_init(mount_path);
167    let mounts = vec![Mount {
168        target: Some("/opt/illa/database".to_string()),
169        source: Some(local_dir),
170        typ: Some(MountTypeEnum::BIND),
171        read_only: Some(false),
172        ..Default::default()
173    }];
174
175    let builder_config = Config {
176        image: Some(builder_image),
177        env: Some(builder_env),
178        labels: Some(builder_labels),
179        host_config: Some(HostConfig {
180            port_bindings: Some(builder_port_bindings),
181            mounts: Some(mounts),
182            ..Default::default()
183        }),
184        ..Default::default()
185    };
186
187    let create_builder = &_docker
188        .create_container(
189            Some(CreateContainerOptions {
190                name: "illa_builder",
191            }),
192            builder_config,
193        )
194        .await;
195
196    let start_builder = &_docker
197        .start_container("illa_builder", None::<StartContainerOptions<String>>)
198        .await;
199
200    match (create_builder.is_err(), start_builder.is_err()) {
201        (true, _) => {
202            pb_deploy.set_style(finish_spinner_style.clone());
203            pb_deploy.finish_with_message(format!(
204                "{} {} {}",
205                ui::emoji::FAIL,
206                String::from("Create ILLA Builder error:"),
207                style(create_builder.as_ref().err().unwrap()).red(),
208            ));
209            process::exit(1);
210        }
211        (false, true) => {
212            pb_deploy.set_style(finish_spinner_style.clone());
213            pb_deploy.finish_with_message(format!(
214                "{} {} {}",
215                ui::emoji::FAIL,
216                String::from("Start ILLA Builder error:"),
217                style(start_builder.as_ref().err().unwrap()).red(),
218            ));
219            process::exit(1);
220        }
221        _ => {
222            pb_deploy.set_style(finish_spinner_style.clone());
223            pb_deploy.finish_with_message(format!(
224                "{} {} {}",
225                ui::emoji::SPARKLE,
226                String::from("ILLA Builder started, please visit"),
227                style(format!("{}:{}", "http://localhost", port)).blue(),
228            ));
229            process::exit(0);
230        }
231    };
232
233    Ok(())
234}
235
236async fn deploy_cloud(progress_style: ProgressStyle) -> Result {
237    println!("{} Looking forward to onboarding you!", ui::emoji::DIAMOND);
238
239    Ok(())
240}