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