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 }