/ src / cli.rs
cli.rs
  1  // SPDX-FileCopyrightText: 2020 Serokell <https://serokell.io/>
  2  // SPDX-FileCopyrightText: 2021 Yannik Sander <contact@ysndr.de>
  3  //
  4  // SPDX-License-Identifier: MPL-2.0
  5  
  6  use std::collections::HashMap;
  7  use std::io::{stdin, stdout, Write};
  8  
  9  use clap::{ArgMatches, Parser, FromArgMatches};
 10  
 11  use crate as deploy;
 12  
 13  use self::deploy::{DeployFlake, ParseFlakeError};
 14  use futures_util::stream::{StreamExt, TryStreamExt};
 15  use log::{debug, error, info, warn};
 16  use serde::Serialize;
 17  use std::path::PathBuf;
 18  use std::process::Stdio;
 19  use thiserror::Error;
 20  use tokio::process::Command;
 21  
 22  /// Simple Rust rewrite of a simple Nix Flake deployment tool
 23  #[derive(Parser, Debug, Clone)]
 24  #[command(version = "1.0", author = "Serokell <https://serokell.io/>")]
 25  pub struct Opts {
 26      /// The flake to deploy
 27      #[arg(group = "deploy")]
 28      target: Option<String>,
 29  
 30      /// A list of flakes to deploy alternatively
 31      #[arg(long, group = "deploy")]
 32      targets: Option<Vec<String>>,
 33      /// Treat targets as files instead of flakes
 34      #[clap(short, long)]
 35      file: Option<String>,
 36      /// Check signatures when using `nix copy`
 37      #[arg(short, long)]
 38      checksigs: bool,
 39      /// Use the interactive prompt before deployment
 40      #[arg(short, long)]
 41      interactive: bool,
 42      /// Extra arguments to be passed to nix build
 43      extra_build_args: Vec<String>,
 44  
 45      /// Print debug logs to output
 46      #[arg(short, long)]
 47      debug_logs: bool,
 48      /// Directory to print logs to (including the background activation process)
 49      #[arg(long)]
 50      log_dir: Option<String>,
 51  
 52      /// Keep the build outputs of each built profile
 53      #[arg(short, long)]
 54      keep_result: bool,
 55      /// Location to keep outputs from built profiles in
 56      #[arg(short, long)]
 57      result_path: Option<String>,
 58  
 59      /// Skip the automatic pre-build checks
 60      #[arg(short, long)]
 61      skip_checks: bool,
 62  
 63      /// Build on remote host
 64      #[arg(long)]
 65      remote_build: bool,
 66  
 67      /// Override the SSH user with the given value
 68      #[arg(long)]
 69      ssh_user: Option<String>,
 70      /// Override the profile user with the given value
 71      #[arg(long)]
 72      profile_user: Option<String>,
 73      /// Override the SSH options used
 74      #[arg(long, allow_hyphen_values = true)]
 75      ssh_opts: Option<String>,
 76      /// Override if the connecting to the target node should be considered fast
 77      #[arg(long)]
 78      fast_connection: Option<bool>,
 79      /// Override if a rollback should be attempted if activation fails
 80      #[arg(long)]
 81      auto_rollback: Option<bool>,
 82      /// Override hostname used for the node
 83      #[arg(long)]
 84      hostname: Option<String>,
 85      /// Make activation wait for confirmation, or roll back after a period of time
 86      #[arg(long)]
 87      magic_rollback: Option<bool>,
 88      /// How long activation should wait for confirmation (if using magic-rollback)
 89      #[arg(long)]
 90      confirm_timeout: Option<u16>,
 91      /// How long we should wait for profile activation
 92      #[arg(long)]
 93      activation_timeout: Option<u16>,
 94      /// Where to store temporary files (only used by magic-rollback)
 95      #[arg(long)]
 96      temp_path: Option<PathBuf>,
 97      /// Show what will be activated on the machines
 98      #[arg(long)]
 99      dry_activate: bool,
100      /// Don't activate, but update the boot loader to boot into the new profile
101      #[arg(long)]
102      boot: bool,
103      /// Revoke all previously succeeded deploys when deploying multiple profiles
104      #[arg(long)]
105      rollback_succeeded: Option<bool>,
106      /// Which sudo command to use. Must accept at least two arguments: user name to execute commands as and the rest is the command to execute
107      #[arg(long)]
108      sudo: Option<String>,
109      /// Prompt for sudo password during activation.
110      #[arg(long)]
111      interactive_sudo: Option<bool>,
112  }
113  
114  /// Returns if the available Nix installation supports flakes
115  async fn test_flake_support() -> Result<bool, std::io::Error> {
116      debug!("Checking for flake support");
117  
118      Ok(Command::new("nix")
119          .arg("eval")
120          .arg("--expr")
121          .arg("builtins.getFlake")
122          // This will error on some machines "intentionally", and we don't really need that printing
123          .stdout(Stdio::null())
124          .stderr(Stdio::null())
125          .status()
126          .await?
127          .success())
128  }
129  
130  #[derive(Error, Debug)]
131  pub enum CheckDeploymentError {
132      #[error("Failed to execute Nix checking command: {0}")]
133      NixCheck(#[from] std::io::Error),
134      #[error("Nix checking command resulted in a bad exit code: {0:?}")]
135      NixCheckExit(Option<i32>),
136  }
137  
138  async fn check_deployment(
139      supports_flakes: bool,
140      repo: &str,
141      extra_build_args: &[String],
142  ) -> Result<(), CheckDeploymentError> {
143      info!("Running checks for flake in {}", repo);
144  
145      let mut check_command = match supports_flakes {
146          true => Command::new("nix"),
147          false => Command::new("nix-build"),
148      };
149  
150      if supports_flakes {
151          check_command.arg("flake").arg("check").arg(repo);
152      } else {
153          check_command.arg("-E")
154                  .arg("--no-out-link")
155                  .arg(format!("let r = import {}/.; x = (if builtins.isFunction r then (r {{}}) else r); in if x ? checks then x.checks.${{builtins.currentSystem}} else {{}}", repo));
156      }
157  
158      check_command.args(extra_build_args);
159  
160      let check_status = check_command.status().await?;
161  
162      match check_status.code() {
163          Some(0) => (),
164          a => return Err(CheckDeploymentError::NixCheckExit(a)),
165      };
166  
167      Ok(())
168  }
169  
170  #[derive(Error, Debug)]
171  pub enum GetDeploymentDataError {
172      #[error("Failed to execute nix eval command: {0}")]
173      NixEval(std::io::Error),
174      #[error("Failed to read output from evaluation: {0}")]
175      NixEvalOut(std::io::Error),
176      #[error("Evaluation resulted in a bad exit code: {0:?}")]
177      NixEvalExit(Option<i32>),
178      #[error("Error converting evaluation output to utf8: {0}")]
179      DecodeUtf8(#[from] std::string::FromUtf8Error),
180      #[error("Error decoding the JSON from evaluation: {0}")]
181      DecodeJson(#[from] serde_json::error::Error),
182      #[error("Impossible happened: profile is set but node is not")]
183      ProfileNoNode,
184  }
185  
186  /// Evaluates the Nix in the given `repo` and return the processed Data from it
187  async fn get_deployment_data(
188      supports_flakes: bool,
189      flakes: &[deploy::DeployFlake<'_>],
190      extra_build_args: &[String],
191  ) -> Result<Vec<deploy::data::Data>, GetDeploymentDataError> {
192      futures_util::stream::iter(flakes).then(|flake| async move {
193  
194      info!("Evaluating flake in {}", flake.repo);
195  
196      let mut c = if supports_flakes {
197          Command::new("nix")
198      } else {
199          Command::new("nix-instantiate")
200      };
201  
202      if supports_flakes {
203          c.arg("eval")
204              .arg("--json")
205              .arg(format!("{}#deploy", flake.repo))
206              // We use --apply instead of --expr so that we don't have to deal with builtins.getFlake
207              .arg("--apply");
208          match (&flake.node, &flake.profile) {
209              (Some(node), Some(profile)) => {
210                  // Ignore all nodes and all profiles but the one we're evaluating
211                  c.arg(format!(
212                      r#"
213                        deploy:
214                        (deploy // {{
215                          nodes = {{
216                            "{0}" = deploy.nodes."{0}" // {{
217                              profiles = {{
218                                inherit (deploy.nodes."{0}".profiles) "{1}";
219                              }};
220                            }};
221                          }};
222                        }})
223                       "#,
224                      node, profile
225                  ))
226              }
227              (Some(node), None) => {
228                  // Ignore all nodes but the one we're evaluating
229                  c.arg(format!(
230                      r#"
231                        deploy:
232                        (deploy // {{
233                          nodes = {{
234                            inherit (deploy.nodes) "{}";
235                          }};
236                        }})
237                      "#,
238                      node
239                  ))
240              }
241              (None, None) => {
242                  // We need to evaluate all profiles of all nodes anyway, so just do it strictly
243                  c.arg("deploy: deploy")
244              }
245              (None, Some(_)) => return Err(GetDeploymentDataError::ProfileNoNode),
246          }
247      } else {
248          c
249              .arg("--strict")
250              .arg("--read-write-mode")
251              .arg("--json")
252              .arg("--eval")
253              .arg("-E")
254              .arg(format!("let r = import {}/.; in if builtins.isFunction r then (r {{}}).deploy else r.deploy", flake.repo))
255      };
256  
257      c.args(extra_build_args);
258  
259      let build_child = c
260          .stdout(Stdio::piped())
261          .spawn()
262          .map_err(GetDeploymentDataError::NixEval)?;
263  
264      let build_output = build_child
265          .wait_with_output()
266          .await
267          .map_err(GetDeploymentDataError::NixEvalOut)?;
268  
269      match build_output.status.code() {
270          Some(0) => (),
271          a => return Err(GetDeploymentDataError::NixEvalExit(a)),
272      };
273  
274      let data_json = String::from_utf8(build_output.stdout)?;
275  
276      Ok(serde_json::from_str(&data_json)?)
277  }).try_collect().await
278  }
279  
280  #[derive(Serialize)]
281  struct PromptPart<'a> {
282      user: &'a str,
283      ssh_user: &'a str,
284      path: &'a str,
285      hostname: &'a str,
286      ssh_opts: &'a [String],
287  }
288  
289  fn print_deployment(
290      parts: &[(
291          &deploy::DeployFlake<'_>,
292          deploy::DeployData,
293          deploy::DeployDefs,
294      )],
295  ) -> Result<(), toml::ser::Error> {
296      let mut part_map: HashMap<String, HashMap<String, PromptPart>> = HashMap::new();
297  
298      for (_, data, defs) in parts {
299          part_map
300              .entry(data.node_name.to_string())
301              .or_insert_with(HashMap::new)
302              .insert(
303                  data.profile_name.to_string(),
304                  PromptPart {
305                      user: &defs.profile_user,
306                      ssh_user: &defs.ssh_user,
307                      path: &data.profile.profile_settings.path,
308                      hostname: &data.node.node_settings.hostname,
309                      ssh_opts: &data.merged_settings.ssh_opts,
310                  },
311              );
312      }
313  
314      let toml = toml::to_string(&part_map)?;
315  
316      info!("The following profiles are going to be deployed:\n{}", toml);
317  
318      Ok(())
319  }
320  #[derive(Error, Debug)]
321  pub enum PromptDeploymentError {
322      #[error("Failed to make printable TOML of deployment: {0}")]
323      TomlFormat(#[from] toml::ser::Error),
324      #[error("Failed to flush stdout prior to query: {0}")]
325      StdoutFlush(std::io::Error),
326      #[error("Failed to read line from stdin: {0}")]
327      StdinRead(std::io::Error),
328      #[error("User cancelled deployment")]
329      Cancelled,
330  }
331  
332  fn prompt_deployment(
333      parts: &[(
334          &deploy::DeployFlake<'_>,
335          deploy::DeployData,
336          deploy::DeployDefs,
337      )],
338  ) -> Result<(), PromptDeploymentError> {
339      print_deployment(parts)?;
340  
341      info!("Are you sure you want to deploy these profiles?");
342      print!("> ");
343  
344      stdout()
345          .flush()
346          .map_err(PromptDeploymentError::StdoutFlush)?;
347  
348      let mut s = String::new();
349      stdin()
350          .read_line(&mut s)
351          .map_err(PromptDeploymentError::StdinRead)?;
352  
353      if !yn::yes(&s) {
354          if yn::is_somewhat_yes(&s) {
355              info!("Sounds like you might want to continue, to be more clear please just say \"yes\". Do you want to deploy these profiles?");
356              print!("> ");
357  
358              stdout()
359                  .flush()
360                  .map_err(PromptDeploymentError::StdoutFlush)?;
361  
362              let mut s = String::new();
363              stdin()
364                  .read_line(&mut s)
365                  .map_err(PromptDeploymentError::StdinRead)?;
366  
367              if !yn::yes(&s) {
368                  return Err(PromptDeploymentError::Cancelled);
369              }
370          } else {
371              if !yn::no(&s) {
372                  info!(
373                      "That was unclear, but sounded like a no to me. Please say \"yes\" or \"no\" to be more clear."
374                  );
375              }
376  
377              return Err(PromptDeploymentError::Cancelled);
378          }
379      }
380  
381      Ok(())
382  }
383  
384  #[derive(Error, Debug)]
385  pub enum RunDeployError {
386      #[error("Failed to deploy profile to node {0}: {1}")]
387      DeployProfile(String, deploy::deploy::DeployProfileError),
388      #[error("Failed to build profile on node {0}: {0}")]
389      BuildProfile(String,  deploy::push::PushProfileError),
390      #[error("Failed to push profile to node {0}: {0}")]
391      PushProfile(String,  deploy::push::PushProfileError),
392      #[error("No profile named `{0}` was found")]
393      ProfileNotFound(String),
394      #[error("No node named `{0}` was found")]
395      NodeNotFound(String),
396      #[error("Profile was provided without a node name")]
397      ProfileWithoutNode,
398      #[error("Error processing deployment definitions: {0}")]
399      DeployDataDefs(#[from] deploy::DeployDataDefsError),
400      #[error("Failed to make printable TOML of deployment: {0}")]
401      TomlFormat(#[from] toml::ser::Error),
402      #[error("{0}")]
403      PromptDeployment(#[from] PromptDeploymentError),
404      #[error("Failed to revoke profile for node {0}: {1}")]
405      RevokeProfile(String, deploy::deploy::RevokeProfileError),
406      #[error("Deployment to node {0} failed, rolled back to previous generation")]
407      Rollback(String)
408  }
409  
410  type ToDeploy<'a> = Vec<(
411      &'a deploy::DeployFlake<'a>,
412      &'a deploy::data::Data,
413      (&'a str, &'a deploy::data::Node),
414      (&'a str, &'a deploy::data::Profile),
415  )>;
416  
417  async fn run_deploy(
418      deploy_flakes: Vec<deploy::DeployFlake<'_>>,
419      data: Vec<deploy::data::Data>,
420      supports_flakes: bool,
421      check_sigs: bool,
422      interactive: bool,
423      cmd_overrides: &deploy::CmdOverrides,
424      keep_result: bool,
425      result_path: Option<&str>,
426      extra_build_args: &[String],
427      debug_logs: bool,
428      dry_activate: bool,
429      boot: bool,
430      log_dir: &Option<String>,
431      rollback_succeeded: bool,
432  ) -> Result<(), RunDeployError> {
433      let to_deploy: ToDeploy = deploy_flakes
434          .iter()
435          .zip(&data)
436          .map(|(deploy_flake, data)| {
437              let to_deploys: ToDeploy = match (&deploy_flake.node, &deploy_flake.profile) {
438                  (Some(node_name), Some(profile_name)) => {
439                      let node = match data.nodes.get(node_name) {
440                          Some(x) => x,
441                          None => return Err(RunDeployError::NodeNotFound(node_name.clone())),
442                      };
443                      let profile = match node.node_settings.profiles.get(profile_name) {
444                          Some(x) => x,
445                          None => return Err(RunDeployError::ProfileNotFound(profile_name.clone())),
446                      };
447  
448                      vec![(
449                          deploy_flake,
450                          data,
451                          (node_name.as_str(), node),
452                          (profile_name.as_str(), profile),
453                      )]
454                  }
455                  (Some(node_name), None) => {
456                      let node = match data.nodes.get(node_name) {
457                          Some(x) => x,
458                          None => return Err(RunDeployError::NodeNotFound(node_name.clone())),
459                      };
460  
461                      let mut profiles_list: Vec<(&str, &deploy::data::Profile)> = Vec::new();
462  
463                      for profile_name in [
464                          node.node_settings.profiles_order.iter().collect(),
465                          node.node_settings.profiles.keys().collect::<Vec<&String>>(),
466                      ]
467                      .concat()
468                      {
469                          let profile = match node.node_settings.profiles.get(profile_name) {
470                              Some(x) => x,
471                              None => {
472                                  return Err(RunDeployError::ProfileNotFound(profile_name.clone()))
473                              }
474                          };
475  
476                          if !profiles_list.iter().any(|(n, _)| n == profile_name) {
477                              profiles_list.push((profile_name, profile));
478                          }
479                      }
480  
481                      profiles_list
482                          .into_iter()
483                          .map(|x| (deploy_flake, data, (node_name.as_str(), node), x))
484                          .collect()
485                  }
486                  (None, None) => {
487                      let mut l = Vec::new();
488  
489                      for (node_name, node) in &data.nodes {
490                          let mut profiles_list: Vec<(&str, &deploy::data::Profile)> = Vec::new();
491  
492                          for profile_name in [
493                              node.node_settings.profiles_order.iter().collect(),
494                              node.node_settings.profiles.keys().collect::<Vec<&String>>(),
495                          ]
496                          .concat()
497                          {
498                              let profile = match node.node_settings.profiles.get(profile_name) {
499                                  Some(x) => x,
500                                  None => {
501                                      return Err(RunDeployError::ProfileNotFound(
502                                          profile_name.clone(),
503                                      ))
504                                  }
505                              };
506  
507                              if !profiles_list.iter().any(|(n, _)| n == profile_name) {
508                                  profiles_list.push((profile_name, profile));
509                              }
510                          }
511  
512                          let ll: ToDeploy = profiles_list
513                              .into_iter()
514                              .map(|x| (deploy_flake, data, (node_name.as_str(), node), x))
515                              .collect();
516  
517                          l.extend(ll);
518                      }
519  
520                      l
521                  }
522                  (None, Some(_)) => return Err(RunDeployError::ProfileWithoutNode),
523              };
524              Ok(to_deploys)
525          })
526          .collect::<Result<Vec<ToDeploy>, RunDeployError>>()?
527          .into_iter()
528          .flatten()
529          .collect();
530  
531      let mut parts: Vec<(
532          &deploy::DeployFlake<'_>,
533          deploy::DeployData,
534          deploy::DeployDefs,
535      )> = Vec::new();
536  
537      for (deploy_flake, data, (node_name, node), (profile_name, profile)) in to_deploy {
538          let deploy_data = deploy::make_deploy_data(
539              &data.generic_settings,
540              node,
541              node_name,
542              profile,
543              profile_name,
544              cmd_overrides,
545              debug_logs,
546              log_dir.as_deref(),
547          );
548  
549          let mut deploy_defs = deploy_data.defs()?;
550  
551          if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) {
552              warn!("Interactive sudo is enabled! Using a sudo password is less secure than correctly configured SSH keys.\nPlease use keys in production environments.");
553  
554              if deploy_data.merged_settings.sudo.is_some() {
555                  warn!("Custom sudo commands should be configured to accept password input from stdin when using the 'interactive sudo' option. Deployment may fail if the custom command ignores stdin.");
556              } else {
557                  // this configures sudo to hide the password prompt and accept input from stdin
558                  // at the time of writing, deploy_defs.sudo defaults to 'sudo -u root' when using user=root and sshUser as non-root
559                  let original = deploy_defs.sudo.unwrap_or("sudo".to_string());
560                  deploy_defs.sudo = Some(format!("{} -S -p \"\"", original));
561              }
562  
563              info!("You will now be prompted for the sudo password for {}.", node.node_settings.hostname);
564              let sudo_password = rpassword::prompt_password(format!("(sudo for {}) Password: ", node.node_settings.hostname)).unwrap_or("".to_string());
565  
566              deploy_defs.sudo_password = Some(sudo_password);
567          }
568  
569          parts.push((deploy_flake, deploy_data, deploy_defs));
570      }
571  
572      if interactive {
573          prompt_deployment(&parts[..])?;
574      } else {
575          print_deployment(&parts[..])?;
576      }
577  
578      let data_iter = || {
579          parts.iter().map(
580              |(deploy_flake, deploy_data, deploy_defs)| deploy::push::PushProfileData {
581                  supports_flakes,
582                  check_sigs,
583                  repo: deploy_flake.repo,
584                  deploy_data,
585                  deploy_defs,
586                  keep_result,
587                  result_path,
588                  extra_build_args,
589              },
590          )
591      };
592  
593      for data in data_iter() {
594          let node_name: String = data.deploy_data.node_name.to_string();
595          deploy::push::build_profile(data).await.map_err(|e| {
596              RunDeployError::BuildProfile(node_name, e)
597          })?;
598      }
599  
600      for data in data_iter() {
601          let node_name: String = data.deploy_data.node_name.to_string();
602          deploy::push::push_profile(data).await.map_err(|e| {
603              RunDeployError::PushProfile(node_name, e)
604          })?;
605      }
606  
607      let mut succeeded: Vec<(&deploy::DeployData, &deploy::DeployDefs)> = vec![];
608  
609      // Run all deployments
610      // In case of an error rollback any previoulsy made deployment.
611      // Rollbacks adhere to the global seeting to auto_rollback and secondary
612      // the profile's configuration
613      for (_, deploy_data, deploy_defs) in &parts {
614          if let Err(e) = deploy::deploy::deploy_profile(deploy_data, deploy_defs, dry_activate, boot).await
615          {
616              error!("{}", e);
617              if dry_activate {
618                  info!("dry run, not rolling back");
619              }
620              if rollback_succeeded && cmd_overrides.auto_rollback.unwrap_or(true) {
621                  info!("Revoking previous deploys");
622                  // revoking all previous deploys
623                  // (adheres to profile configuration if not set explicitely by
624                  //  the command line)
625                  for (deploy_data, deploy_defs) in &succeeded {
626                      if deploy_data.merged_settings.auto_rollback.unwrap_or(true) {
627                          deploy::deploy::revoke(*deploy_data, *deploy_defs).await.map_err(|e| {
628                              RunDeployError::RevokeProfile(deploy_data.node_name.to_string(), e)
629                          })?;
630                      }
631                  }
632                  return Err(RunDeployError::Rollback(deploy_data.node_name.to_string()));
633              }
634              return Err(RunDeployError::DeployProfile(deploy_data.node_name.to_string(), e))
635          }
636          succeeded.push((deploy_data, deploy_defs))
637      }
638  
639      Ok(())
640  }
641  
642  #[derive(Error, Debug)]
643  pub enum RunError {
644      #[error("Failed to deploy profile: {0}")]
645      DeployProfile(#[from] deploy::deploy::DeployProfileError),
646      #[error("Failed to push profile: {0}")]
647      PushProfile(#[from] deploy::push::PushProfileError),
648      #[error("Failed to test for flake support: {0}")]
649      FlakeTest(std::io::Error),
650      #[error("Failed to check deployment: {0}")]
651      CheckDeployment(#[from] CheckDeploymentError),
652      #[error("Failed to evaluate deployment data: {0}")]
653      GetDeploymentData(#[from] GetDeploymentDataError),
654      #[error("Error parsing flake: {0}")]
655      ParseFlake(#[from] deploy::ParseFlakeError),
656      #[error("Error parsing arguments: {0}")]
657      ParseArgs(#[from] clap::Error),
658      #[error("Error initiating logger: {0}")]
659      Logger(#[from] flexi_logger::FlexiLoggerError),
660      #[error("{0}")]
661      RunDeploy(#[from] RunDeployError),
662  }
663  
664  pub async fn run(args: Option<&ArgMatches>) -> Result<(), RunError> {
665      let opts = match args {
666          Some(o) => <Opts as FromArgMatches>::from_arg_matches(o)?,
667          None => Opts::parse(),
668      };
669  
670      deploy::init_logger(
671          opts.debug_logs,
672          opts.log_dir.as_deref(),
673          &deploy::LoggerType::Deploy,
674      )?;
675  
676      if opts.dry_activate && opts.boot {
677          error!("Cannot use both --dry-activate & --boot!");
678      }
679  
680      let deploys = opts
681          .clone()
682          .targets
683          .unwrap_or_else(|| vec![opts.clone().target.unwrap_or_else(|| ".".to_string())]);
684  
685      let deploy_flakes: Vec<DeployFlake> =
686          if let Some(file) = &opts.file {
687              deploys
688                  .iter()
689                  .map(|f| deploy::parse_file(file.as_str(), f.as_str()))
690                  .collect::<Result<Vec<DeployFlake>, ParseFlakeError>>()?
691          }
692      else {
693          deploys
694          .iter()
695          .map(|f| deploy::parse_flake(f.as_str()))
696            .collect::<Result<Vec<DeployFlake>, ParseFlakeError>>()?
697      };
698  
699      let cmd_overrides = deploy::CmdOverrides {
700          ssh_user: opts.ssh_user,
701          profile_user: opts.profile_user,
702          ssh_opts: opts.ssh_opts,
703          fast_connection: opts.fast_connection,
704          auto_rollback: opts.auto_rollback,
705          hostname: opts.hostname,
706          magic_rollback: opts.magic_rollback,
707          temp_path: opts.temp_path,
708          confirm_timeout: opts.confirm_timeout,
709          activation_timeout: opts.activation_timeout,
710          dry_activate: opts.dry_activate,
711          remote_build: opts.remote_build,
712          sudo: opts.sudo,
713          interactive_sudo: opts.interactive_sudo
714      };
715  
716      let supports_flakes = test_flake_support().await.map_err(RunError::FlakeTest)?;
717      let do_not_want_flakes = opts.file.is_some();
718  
719      if !supports_flakes {
720          warn!("A Nix version without flakes support was detected, support for this is work in progress");
721      }
722  
723      if do_not_want_flakes {
724          warn!("The --file option for deployments without flakes is experimental");
725      }
726  
727      let using_flakes = supports_flakes && !do_not_want_flakes;
728  
729      if !opts.skip_checks {
730          for deploy_flake in &deploy_flakes {
731              check_deployment(using_flakes, deploy_flake.repo, &opts.extra_build_args).await?;
732          }
733      }
734      let result_path = opts.result_path.as_deref();
735      let data = get_deployment_data(using_flakes, &deploy_flakes, &opts.extra_build_args).await?;
736      run_deploy(
737          deploy_flakes,
738          data,
739          using_flakes,
740          opts.checksigs,
741          opts.interactive,
742          &cmd_overrides,
743          opts.keep_result,
744          result_path,
745          &opts.extra_build_args,
746          opts.debug_logs,
747          opts.dry_activate,
748          opts.boot,
749          &opts.log_dir,
750          opts.rollback_succeeded.unwrap_or(true),
751      )
752      .await?;
753  
754      Ok(())
755  }