// SPDX-FileCopyrightText: 2020 Serokell
//
// SPDX-License-Identifier: MPL-2.0
use clap::Clap;
use std::process::Stdio;
use tokio::process::Command;
use merge::Merge;
extern crate pretty_env_logger;
#[macro_use]
extern crate log;
#[macro_use]
extern crate serde_derive;
#[macro_use]
mod utils;
/// Simple Rust rewrite of a simple Nix Flake deployment tool
#[derive(Clap, Debug)]
#[clap(version = "1.0", author = "Serokell ")]
struct Opts {
/// The flake to deploy
#[clap(default_value = ".")]
flake: String,
/// Check signatures when using `nix copy`
#[clap(short, long)]
checksigs: bool,
/// Extra arguments to be passed to nix build
extra_build_args: Vec,
}
#[inline]
async fn push_all_profiles(
node: &utils::data::Node,
node_name: &str,
supports_flakes: bool,
repo: &str,
top_settings: &utils::data::GenericSettings,
check_sigs: bool,
) -> Result<(), Box> {
info!("Pushing all profiles for `{}`", node_name);
let mut profiles_list: Vec<&str> = node.profiles_order.iter().map(|x| x.as_ref()).collect();
// Add any profiles which weren't in the provided order list
for profile_name in node.profiles.keys() {
if !profiles_list.contains(&profile_name.as_str()) {
profiles_list.push(&profile_name);
}
}
for profile_name in profiles_list {
let profile = match node.profiles.get(profile_name) {
Some(x) => x,
None => good_panic!("No profile was found named `{}`", profile_name),
};
let mut merged_settings = top_settings.clone();
merged_settings.merge(node.generic_settings.clone());
merged_settings.merge(profile.generic_settings.clone());
let deploy_data =
utils::make_deploy_data(profile_name, node_name, &merged_settings).await?;
utils::push::push_profile(
profile,
profile_name,
node,
node_name,
supports_flakes,
check_sigs,
repo,
&merged_settings,
&deploy_data,
)
.await?;
}
Ok(())
}
#[inline]
async fn deploy_all_profiles(
node: &utils::data::Node,
node_name: &str,
top_settings: &utils::data::GenericSettings,
) -> Result<(), Box> {
info!("Deploying all profiles for `{}`", node_name);
let mut profiles_list: Vec<&str> = node.profiles_order.iter().map(|x| x.as_ref()).collect();
// Add any profiles which weren't in the provided order list
for profile_name in node.profiles.keys() {
if !profiles_list.contains(&profile_name.as_str()) {
profiles_list.push(&profile_name);
}
}
for profile_name in profiles_list {
let profile = match node.profiles.get(profile_name) {
Some(x) => x,
None => good_panic!("No profile was found named `{}`", profile_name),
};
let mut merged_settings = top_settings.clone();
merged_settings.merge(node.generic_settings.clone());
merged_settings.merge(profile.generic_settings.clone());
let deploy_data =
utils::make_deploy_data(profile_name, node_name, &merged_settings).await?;
utils::deploy::deploy_profile(
profile,
profile_name,
node,
node_name,
&merged_settings,
&deploy_data,
merged_settings.auto_rollback,
)
.await?;
}
Ok(())
}
/// Returns if the available Nix installation supports flakes
#[inline]
async fn test_flake_support() -> Result> {
Ok(Command::new("nix")
.arg("eval")
.arg("--expr")
.arg("builtins.getFlake")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await?
.success())
}
/// Evaluates the Nix in the given `repo` and return the processed Data from it
#[inline]
async fn get_deployment_data(
supports_flakes: bool,
repo: &str,
extra_build_args: &[String],
) -> Result> {
let mut c = match supports_flakes {
true => Command::new("nix"),
false => Command::new("nix-instanciate"),
};
let mut build_command = match supports_flakes {
true => {
c.arg("eval").arg("--json").arg(format!("{}#deploy", repo))
}
false => {
c
.arg("--strict")
.arg("--read-write-mode")
.arg("--json")
.arg("--eval")
.arg("--E")
.arg(format!("let r = import {}/.; in if builtins.isFunction r then (r {{}}).deploy else r.deploy", repo))
}
};
for extra_arg in extra_build_args {
build_command = build_command.arg(extra_arg);
}
let build_output = build_command
.stdout(Stdio::null())
.stderr(Stdio::null())
.output()
.await?;
if !build_output.status.success() {
good_panic!(
"Error building deploy props for the provided flake: {}",
repo
);
}
let data_json = String::from_utf8(build_output.stdout)?;
Ok(serde_json::from_str(&data_json)?)
}
async fn run_deploy(
deploy_flake: utils::DeployFlake<'_>,
data: utils::data::Data,
supports_flakes: bool,
opts: &Opts,
) -> Result<(), Box> {
match (deploy_flake.node, deploy_flake.profile) {
(Some(node_name), Some(profile_name)) => {
let node = match data.nodes.get(node_name) {
Some(x) => x,
None => good_panic!("No node was found named `{}`", node_name),
};
let profile = match node.profiles.get(profile_name) {
Some(x) => x,
None => good_panic!("No profile was found named `{}`", profile_name),
};
let mut merged_settings = data.generic_settings.clone();
merged_settings.merge(node.generic_settings.clone());
merged_settings.merge(profile.generic_settings.clone());
let deploy_data =
utils::make_deploy_data(profile_name, node_name, &merged_settings).await?;
utils::push::push_profile(
profile,
profile_name,
node,
node_name,
supports_flakes,
opts.checksigs,
deploy_flake.repo,
&merged_settings,
&deploy_data,
)
.await?;
utils::deploy::deploy_profile(
profile,
profile_name,
node,
node_name,
&merged_settings,
&deploy_data,
merged_settings.auto_rollback,
)
.await?;
}
(Some(node_name), None) => {
let node = match data.nodes.get(node_name) {
Some(x) => x,
None => good_panic!("No node was found named `{}`", node_name),
};
push_all_profiles(
node,
node_name,
supports_flakes,
deploy_flake.repo,
&data.generic_settings,
opts.checksigs,
)
.await?;
deploy_all_profiles(node, node_name, &data.generic_settings).await?;
}
(None, None) => {
info!("Deploying all profiles on all nodes");
for (node_name, node) in &data.nodes {
push_all_profiles(
node,
node_name,
supports_flakes,
deploy_flake.repo,
&data.generic_settings,
opts.checksigs,
)
.await?;
}
for (node_name, node) in &data.nodes {
deploy_all_profiles(node, node_name, &data.generic_settings).await?;
}
}
(None, Some(_)) => {
good_panic!("Profile provided without a node, this is not (currently) supported")
}
};
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Box> {
if std::env::var("DEPLOY_LOG").is_err() {
std::env::set_var("DEPLOY_LOG", "info");
}
pretty_env_logger::init_custom_env("DEPLOY_LOG");
let opts: Opts = Opts::parse();
let deploy_flake = utils::parse_flake(opts.flake.as_str());
let supports_flakes = test_flake_support().await?;
let data =
get_deployment_data(supports_flakes, deploy_flake.repo, &opts.extra_build_args).await?;
run_deploy(deploy_flake, data, supports_flakes, &opts).await?;
Ok(())
}