1use crate::{
8 ansible::{
9 inventory::cleanup_environment_inventory,
10 provisioning::{AnsibleProvisioner, ProvisionOptions},
11 AnsibleRunner,
12 },
13 error::{Error, Result},
14 get_environment_details,
15 infra::ClientsInfraRunOptions,
16 inventory::ClientsDeploymentInventory,
17 print_duration,
18 s3::S3Repository,
19 ssh::SshClient,
20 terraform::TerraformRunner,
21 write_environment_details, BinaryOption, CloudProvider, DeploymentType, EnvironmentDetails,
22 EnvironmentType, EvmDetails,
23};
24use alloy::primitives::U256;
25use serde::{Deserialize, Serialize};
26use std::{path::PathBuf, time::Instant};
27
28const ANSIBLE_DEFAULT_FORKS: usize = 50;
29
30#[derive(Clone, Serialize, Deserialize)]
31pub struct ClientsDeployOptions {
32 pub binary_option: BinaryOption,
33 pub chunk_size: Option<u64>,
34 pub client_env_variables: Option<Vec<(String, String)>>,
35 pub client_vm_count: Option<u16>,
36 pub client_vm_size: Option<String>,
37 pub current_inventory: ClientsDeploymentInventory,
38 pub enable_downloaders: bool,
39 pub enable_telegraf: bool,
40 pub environment_type: EnvironmentType,
41 pub evm_details: EvmDetails,
42 pub funding_wallet_secret_key: Option<String>,
43 pub initial_gas: Option<U256>,
44 pub initial_tokens: Option<U256>,
45 pub max_archived_log_files: u16,
46 pub max_log_files: u16,
47 pub max_uploads: Option<u32>,
48 pub name: String,
49 pub network_id: Option<u8>,
50 pub network_contacts_url: String,
51 pub output_inventory_dir_path: PathBuf,
52 pub peer: String,
53 pub uploaders_count: u16,
54 pub wallet_secret_keys: Option<Vec<String>>,
55}
56
57#[derive(Default)]
58pub struct ClientsDeployBuilder {
59 ansible_forks: Option<usize>,
60 ansible_verbose_mode: bool,
61 deployment_type: EnvironmentType,
62 environment_name: String,
63 provider: Option<CloudProvider>,
64 ssh_secret_key_path: Option<PathBuf>,
65 state_bucket_name: Option<String>,
66 terraform_binary_path: Option<PathBuf>,
67 vault_password_path: Option<PathBuf>,
68 working_directory_path: Option<PathBuf>,
69}
70
71impl ClientsDeployBuilder {
72 pub fn new() -> Self {
73 Default::default()
74 }
75
76 pub fn ansible_verbose_mode(&mut self, ansible_verbose_mode: bool) -> &mut Self {
77 self.ansible_verbose_mode = ansible_verbose_mode;
78 self
79 }
80
81 pub fn ansible_forks(&mut self, ansible_forks: usize) -> &mut Self {
82 self.ansible_forks = Some(ansible_forks);
83 self
84 }
85
86 pub fn deployment_type(&mut self, deployment_type: EnvironmentType) -> &mut Self {
87 self.deployment_type = deployment_type;
88 self
89 }
90
91 pub fn environment_name(&mut self, name: &str) -> &mut Self {
92 self.environment_name = name.to_string();
93 self
94 }
95
96 pub fn provider(&mut self, provider: CloudProvider) -> &mut Self {
97 self.provider = Some(provider);
98 self
99 }
100
101 pub fn state_bucket_name(&mut self, state_bucket_name: String) -> &mut Self {
102 self.state_bucket_name = Some(state_bucket_name);
103 self
104 }
105
106 pub fn terraform_binary_path(&mut self, terraform_binary_path: PathBuf) -> &mut Self {
107 self.terraform_binary_path = Some(terraform_binary_path);
108 self
109 }
110
111 pub fn working_directory(&mut self, working_directory_path: PathBuf) -> &mut Self {
112 self.working_directory_path = Some(working_directory_path);
113 self
114 }
115
116 pub fn ssh_secret_key_path(&mut self, ssh_secret_key_path: PathBuf) -> &mut Self {
117 self.ssh_secret_key_path = Some(ssh_secret_key_path);
118 self
119 }
120
121 pub fn vault_password_path(&mut self, vault_password_path: PathBuf) -> &mut Self {
122 self.vault_password_path = Some(vault_password_path);
123 self
124 }
125
126 pub fn build(&self) -> Result<ClientsDeployer> {
127 let provider = self.provider.unwrap_or(CloudProvider::DigitalOcean);
128 match provider {
129 CloudProvider::DigitalOcean => {
130 let digital_ocean_pat = std::env::var("DO_PAT").map_err(|_| {
131 Error::CloudProviderCredentialsNotSupplied("DO_PAT".to_string())
132 })?;
133 std::env::set_var("DIGITALOCEAN_TOKEN", digital_ocean_pat.clone());
137 std::env::set_var("DO_API_TOKEN", digital_ocean_pat);
138 }
139 _ => {
140 return Err(Error::CloudProviderNotSupported(provider.to_string()));
141 }
142 }
143
144 let state_bucket_name = match self.state_bucket_name {
145 Some(ref bucket_name) => bucket_name.clone(),
146 None => std::env::var("CLIENT_TERRAFORM_STATE_BUCKET_NAME")?,
147 };
148
149 let default_terraform_bin_path = PathBuf::from("terraform");
150 let terraform_binary_path = self
151 .terraform_binary_path
152 .as_ref()
153 .unwrap_or(&default_terraform_bin_path);
154
155 let working_directory_path = match self.working_directory_path {
156 Some(ref work_dir_path) => work_dir_path.clone(),
157 None => std::env::current_dir()?.join("resources"),
158 };
159
160 let ssh_secret_key_path = match self.ssh_secret_key_path {
161 Some(ref ssh_sk_path) => ssh_sk_path.clone(),
162 None => PathBuf::from(std::env::var("SSH_KEY_PATH")?),
163 };
164
165 let vault_password_path = match self.vault_password_path {
166 Some(ref vault_pw_path) => vault_pw_path.clone(),
167 None => PathBuf::from(std::env::var("ANSIBLE_VAULT_PASSWORD_PATH")?),
168 };
169
170 let terraform_runner = TerraformRunner::new(
171 terraform_binary_path.to_path_buf(),
172 working_directory_path
173 .join("terraform")
174 .join("clients")
175 .join(provider.to_string()),
176 provider,
177 &state_bucket_name,
178 )?;
179
180 let ansible_runner = AnsibleRunner::new(
181 self.ansible_forks.unwrap_or(ANSIBLE_DEFAULT_FORKS),
182 self.ansible_verbose_mode,
183 &self.environment_name,
184 provider,
185 ssh_secret_key_path.clone(),
186 vault_password_path,
187 working_directory_path.join("ansible"),
188 )?;
189
190 let ssh_client = SshClient::new(ssh_secret_key_path);
191 let ansible_provisioner =
192 AnsibleProvisioner::new(ansible_runner, provider, ssh_client.clone());
193
194 let client_deployer = ClientsDeployer::new(
195 ansible_provisioner,
196 provider,
197 self.deployment_type.clone(),
198 &self.environment_name,
199 S3Repository {},
200 ssh_client,
201 terraform_runner,
202 working_directory_path,
203 )?;
204
205 Ok(client_deployer)
206 }
207}
208
209#[derive(Clone)]
210pub struct ClientsDeployer {
211 pub ansible_provisioner: AnsibleProvisioner,
212 pub cloud_provider: CloudProvider,
213 pub deployment_type: EnvironmentType,
214 pub environment_name: String,
215 pub inventory_file_path: PathBuf,
216 pub s3_repository: S3Repository,
217 pub ssh_client: SshClient,
218 pub terraform_runner: TerraformRunner,
219 pub working_directory_path: PathBuf,
220}
221
222impl ClientsDeployer {
223 #[allow(clippy::too_many_arguments)]
224 pub fn new(
225 ansible_provisioner: AnsibleProvisioner,
226 cloud_provider: CloudProvider,
227 deployment_type: EnvironmentType,
228 environment_name: &str,
229 s3_repository: S3Repository,
230 ssh_client: SshClient,
231 terraform_runner: TerraformRunner,
232 working_directory_path: PathBuf,
233 ) -> Result<ClientsDeployer> {
234 if environment_name.is_empty() {
235 return Err(Error::EnvironmentNameRequired);
236 }
237 let inventory_file_path = working_directory_path
238 .join("ansible")
239 .join("inventory")
240 .join("dev_inventory_digital_ocean.yml");
241
242 Ok(ClientsDeployer {
243 ansible_provisioner,
244 cloud_provider,
245 deployment_type,
246 environment_name: environment_name.to_string(),
247 inventory_file_path,
248 s3_repository,
249 ssh_client,
250 terraform_runner,
251 working_directory_path,
252 })
253 }
254
255 pub fn create_or_update_infra(&self, options: &ClientsInfraRunOptions) -> Result<()> {
256 let start = Instant::now();
257 println!("Selecting {} workspace...", options.name);
258 self.terraform_runner.workspace_select(&options.name)?;
259
260 let args = options.build_terraform_args()?;
261
262 println!("Running terraform apply...");
263 self.terraform_runner
264 .apply(args, Some(options.tfvars_filename.clone()))?;
265 print_duration(start.elapsed());
266 Ok(())
267 }
268
269 pub async fn init(&self) -> Result<()> {
270 self.terraform_runner.init()?;
271 let workspaces = self.terraform_runner.workspace_list()?;
272 if !workspaces.contains(&self.environment_name) {
273 self.terraform_runner
274 .workspace_new(&self.environment_name)?;
275 } else {
276 println!("Workspace {} already exists", self.environment_name);
277 }
278
279 Ok(())
280 }
281
282 pub fn plan(&self, options: &ClientsInfraRunOptions) -> Result<()> {
283 println!("Selecting {} workspace...", options.name);
284 self.terraform_runner.workspace_select(&options.name)?;
285
286 let args = options.build_terraform_args()?;
287
288 self.terraform_runner
289 .plan(Some(args), Some(options.tfvars_filename.clone()))?;
290 Ok(())
291 }
292
293 pub async fn deploy(&self, options: ClientsDeployOptions) -> Result<()> {
294 println!(
295 "Deploying client for environment: {}",
296 self.environment_name
297 );
298
299 let build_custom_binaries = {
300 match &options.binary_option {
301 BinaryOption::BuildFromSource { .. } => true,
302 BinaryOption::Versioned { .. } => false,
303 }
304 };
305
306 let start = Instant::now();
307 println!("Initializing infrastructure...");
308
309 let infra_options = ClientsInfraRunOptions {
310 client_image_id: None,
311 client_vm_count: options.client_vm_count,
312 client_vm_size: options.client_vm_size.clone(),
313 enable_build_vm: build_custom_binaries,
314 name: options.name.clone(),
315 tfvars_filename: options.current_inventory.get_tfvars_filename(),
316 };
317
318 self.create_or_update_infra(&infra_options)?;
319
320 write_environment_details(
321 &self.s3_repository,
322 &options.name,
323 &EnvironmentDetails {
324 deployment_type: DeploymentType::Client,
325 environment_type: options.environment_type.clone(),
326 evm_details: EvmDetails {
327 network: options.evm_details.network.clone(),
328 data_payments_address: options.evm_details.data_payments_address.clone(),
329 payment_token_address: options.evm_details.payment_token_address.clone(),
330 rpc_url: options.evm_details.rpc_url.clone(),
331 },
332 funding_wallet_address: None,
333 network_id: options.network_id,
334 rewards_address: None,
335 },
336 )
337 .await?;
338
339 println!("Provisioning Client with Ansible...");
340 let provision_options = ProvisionOptions::from(options.clone());
341
342 if build_custom_binaries {
343 self.ansible_provisioner
344 .print_ansible_run_banner("Build Custom Binaries");
345 self.ansible_provisioner
346 .build_safe_network_binaries(&provision_options, Some(vec!["ant".to_string()]))
347 .map_err(|err| {
348 println!("Failed to build safe network binaries {err:?}");
349 err
350 })?;
351 }
352
353 self.ansible_provisioner
354 .print_ansible_run_banner("Provision Clients");
355 self.ansible_provisioner
356 .provision_clients(
357 &provision_options,
358 Some(options.peer.clone()),
359 Some(options.network_contacts_url.clone()),
360 )
361 .await
362 .map_err(|err| {
363 println!("Failed to provision Clients {err:?}");
364 err
365 })?;
366
367 self.ansible_provisioner
368 .print_ansible_run_banner("Provision Downloaders");
369 self.ansible_provisioner
370 .provision_downloaders(
371 &provision_options,
372 Some(options.peer.clone()),
373 Some(options.network_contacts_url.clone()),
374 )
375 .await
376 .map_err(|err| {
377 println!("Failed to provision downloaders {err:?}");
378 err
379 })?;
380
381 println!("Deployment completed successfully in {:?}", start.elapsed());
382 Ok(())
383 }
384
385 async fn destroy_infra(&self, environment_details: &EnvironmentDetails) -> Result<()> {
386 crate::infra::select_workspace(&self.terraform_runner, &self.environment_name)?;
387
388 let options = ClientsInfraRunOptions::generate_existing(
389 &self.environment_name,
390 &self.terraform_runner,
391 environment_details,
392 )
393 .await?;
394
395 let mut args = Vec::new();
396 if let Some(vm_count) = options.client_vm_count {
397 args.push(("ant_client_vm_count".to_string(), vm_count.to_string()));
398 }
399 if let Some(vm_size) = &options.client_vm_size {
400 args.push(("ant_client_droplet_size".to_string(), vm_size.clone()));
401 }
402 args.push((
403 "use_custom_bin".to_string(),
404 options.enable_build_vm.to_string(),
405 ));
406
407 self.terraform_runner
408 .destroy(Some(args), Some(options.tfvars_filename.clone()))?;
409
410 crate::infra::delete_workspace(&self.terraform_runner, &self.environment_name)?;
411
412 Ok(())
413 }
414
415 pub async fn clean(&self) -> Result<()> {
416 let environment_details =
417 get_environment_details(&self.environment_name, &self.s3_repository).await?;
418 crate::funding::drain_funds(&self.ansible_provisioner, &environment_details).await?;
419
420 self.destroy_infra(&environment_details).await?;
421
422 cleanup_environment_inventory(
423 &self.environment_name,
424 &self
425 .working_directory_path
426 .join("ansible")
427 .join("inventory"),
428 None,
429 )?;
430
431 self.s3_repository
432 .delete_object("sn-environment-type", &self.environment_name)
433 .await?;
434 Ok(())
435 }
436}