Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref delploy data aka target parser III/V #119

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 43 additions & 43 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ use clap::{ArgMatches, Clap, FromArgMatches};

use crate as deploy;

use self::deploy::{DeployFlake, ParseFlakeError};
use self::deploy::{data, settings};
use futures_util::stream::{StreamExt, TryStreamExt};
use log::{debug, error, info, warn};
use serde::Serialize;
@@ -168,9 +168,9 @@ pub enum GetDeploymentDataError {
/// Evaluates the Nix in the given `repo` and return the processed Data from it
async fn get_deployment_data(
supports_flakes: bool,
flakes: &[deploy::DeployFlake<'_>],
flakes: &[data::Target],
extra_build_args: &[String],
) -> Result<Vec<deploy::data::Data>, GetDeploymentDataError> {
) -> Result<Vec<settings::Root>, GetDeploymentDataError> {
futures_util::stream::iter(flakes).then(|flake| async move {

info!("Evaluating flake in {}", flake.repo);
@@ -272,9 +272,9 @@ struct PromptPart<'a> {

fn print_deployment(
parts: &[(
&deploy::DeployFlake<'_>,
deploy::DeployData,
deploy::DeployDefs,
&data::Target,
data::DeployData,
data::DeployDefs,
)],
) -> Result<(), toml::ser::Error> {
let mut part_map: HashMap<String, HashMap<String, PromptPart>> = HashMap::new();
@@ -315,9 +315,9 @@ pub enum PromptDeploymentError {

fn prompt_deployment(
parts: &[(
&deploy::DeployFlake<'_>,
deploy::DeployData,
deploy::DeployDefs,
&data::Target,
data::DeployData,
data::DeployDefs,
)],
) -> Result<(), PromptDeploymentError> {
print_deployment(parts)?;
@@ -378,7 +378,7 @@ pub enum RunDeployError {
#[error("Profile was provided without a node name")]
ProfileWithoutNode,
#[error("Error processing deployment definitions: {0}")]
DeployDataDefs(#[from] deploy::DeployDataDefsError),
InvalidDeployDataDefs(#[from] data::DeployDataDefsError),
#[error("Failed to make printable TOML of deployment: {0}")]
TomlFormat(#[from] toml::ser::Error),
#[error("{0}")]
@@ -388,19 +388,19 @@ pub enum RunDeployError {
}

type ToDeploy<'a> = Vec<(
&'a deploy::DeployFlake<'a>,
&'a deploy::data::Data,
(&'a str, &'a deploy::data::Node),
(&'a str, &'a deploy::data::Profile),
&'a data::Target,
&'a settings::Root,
(&'a str, &'a settings::Node),
(&'a str, &'a settings::Profile),
)>;

async fn run_deploy(
deploy_flakes: Vec<deploy::DeployFlake<'_>>,
data: Vec<deploy::data::Data>,
deploy_targets: Vec<data::Target>,
data: Vec<settings::Root>,
supports_flakes: bool,
check_sigs: bool,
interactive: bool,
cmd_overrides: &deploy::CmdOverrides,
cmd_overrides: &data::CmdOverrides,
keep_result: bool,
result_path: Option<&str>,
extra_build_args: &[String],
@@ -409,11 +409,11 @@ async fn run_deploy(
log_dir: &Option<String>,
rollback_succeeded: bool,
) -> Result<(), RunDeployError> {
let to_deploy: ToDeploy = deploy_flakes
let to_deploy: ToDeploy = deploy_targets
.iter()
.zip(&data)
.map(|(deploy_flake, data)| {
let to_deploys: ToDeploy = match (&deploy_flake.node, &deploy_flake.profile) {
.map(|(deploy_target, data)| {
let to_deploys: ToDeploy = match (&deploy_target.node, &deploy_target.profile) {
(Some(node_name), Some(profile_name)) => {
let node = match data.nodes.get(node_name) {
Some(x) => x,
@@ -425,7 +425,7 @@ async fn run_deploy(
};

vec![(
deploy_flake,
deploy_target,
data,
(node_name.as_str(), node),
(profile_name.as_str(), profile),
@@ -437,7 +437,7 @@ async fn run_deploy(
None => return Err(RunDeployError::NodeNotFound(node_name.clone())),
};

let mut profiles_list: Vec<(&str, &deploy::data::Profile)> = Vec::new();
let mut profiles_list: Vec<(&str, &settings::Profile)> = Vec::new();

for profile_name in [
node.node_settings.profiles_order.iter().collect(),
@@ -459,14 +459,14 @@ async fn run_deploy(

profiles_list
.into_iter()
.map(|x| (deploy_flake, data, (node_name.as_str(), node), x))
.map(|x| (deploy_target, data, (node_name.as_str(), node), x))
.collect()
}
(None, None) => {
let mut l = Vec::new();

for (node_name, node) in &data.nodes {
let mut profiles_list: Vec<(&str, &deploy::data::Profile)> = Vec::new();
let mut profiles_list: Vec<(&str, &settings::Profile)> = Vec::new();

for profile_name in [
node.node_settings.profiles_order.iter().collect(),
@@ -490,7 +490,7 @@ async fn run_deploy(

let ll: ToDeploy = profiles_list
.into_iter()
.map(|x| (deploy_flake, data, (node_name.as_str(), node), x))
.map(|x| (deploy_target, data, (node_name.as_str(), node), x))
.collect();

l.extend(ll);
@@ -508,13 +508,13 @@ async fn run_deploy(
.collect();

let mut parts: Vec<(
&deploy::DeployFlake<'_>,
deploy::DeployData,
deploy::DeployDefs,
&data::Target,
data::DeployData,
data::DeployDefs,
)> = Vec::new();

for (deploy_flake, data, (node_name, node), (profile_name, profile)) in to_deploy {
let deploy_data = deploy::make_deploy_data(
for (deploy_target, data, (node_name, node), (profile_name, profile)) in to_deploy {
let deploy_data = data::make_deploy_data(
&data.generic_settings,
node,
node_name,
@@ -527,7 +527,7 @@ async fn run_deploy(

let deploy_defs = deploy_data.defs()?;

parts.push((deploy_flake, deploy_data, deploy_defs));
parts.push((deploy_target, deploy_data, deploy_defs));
}

if interactive {
@@ -536,11 +536,11 @@ async fn run_deploy(
print_deployment(&parts[..])?;
}

for (deploy_flake, deploy_data, deploy_defs) in &parts {
for (deploy_target, deploy_data, deploy_defs) in &parts {
deploy::push::push_profile(deploy::push::PushProfileData {
supports_flakes,
check_sigs,
repo: deploy_flake.repo,
repo: &deploy_target.repo,
deploy_data,
deploy_defs,
keep_result,
@@ -550,7 +550,7 @@ async fn run_deploy(
.await?;
}

let mut succeeded: Vec<(&deploy::DeployData, &deploy::DeployDefs)> = vec![];
let mut succeeded: Vec<(&data::DeployData, &data::DeployDefs)> = vec![];

// Run all deployments
// In case of an error rollback any previoulsy made deployment.
@@ -595,7 +595,7 @@ pub enum RunError {
#[error("Failed to evaluate deployment data: {0}")]
GetDeploymentData(#[from] GetDeploymentDataError),
#[error("Error parsing flake: {0}")]
ParseFlake(#[from] deploy::ParseFlakeError),
ParseFlake(#[from] data::ParseTargetError),
#[error("Error initiating logger: {0}")]
Logger(#[from] flexi_logger::FlexiLoggerError),
#[error("{0}")]
@@ -619,12 +619,12 @@ pub async fn run(args: Option<&ArgMatches>) -> Result<(), RunError> {
.targets
.unwrap_or_else(|| vec![opts.clone().target.unwrap_or_else(|| ".".to_string())]);

let deploy_flakes: Vec<DeployFlake> = deploys
let deploy_targets: Vec<data::Target> = deploys
.iter()
.map(|f| deploy::parse_flake(f.as_str()))
.collect::<Result<Vec<DeployFlake>, ParseFlakeError>>()?;
.map(|f| f.parse::<data::Target>())
.collect::<Result<Vec<data::Target>, data::ParseTargetError>>()?;

let cmd_overrides = deploy::CmdOverrides {
let cmd_overrides = data::CmdOverrides {
ssh_user: opts.ssh_user,
profile_user: opts.profile_user,
ssh_opts: opts.ssh_opts,
@@ -644,14 +644,14 @@ pub async fn run(args: Option<&ArgMatches>) -> Result<(), RunError> {
}

if !opts.skip_checks {
for deploy_flake in &deploy_flakes {
check_deployment(supports_flakes, deploy_flake.repo, &opts.extra_build_args).await?;
for deploy_target in deploy_targets.iter() {
check_deployment(supports_flakes, &deploy_target.repo, &opts.extra_build_args).await?;
}
}
let result_path = opts.result_path.as_deref();
let data = get_deployment_data(supports_flakes, &deploy_flakes, &opts.extra_build_args).await?;
let data = get_deployment_data(supports_flakes, &deploy_targets, &opts.extra_build_args).await?;
run_deploy(
deploy_flakes,
deploy_targets,
data,
supports_flakes,
opts.checksigs,
334 changes: 282 additions & 52 deletions src/data.rs
Original file line number Diff line number Diff line change
@@ -1,73 +1,303 @@
// SPDX-FileCopyrightText: 2020 Serokell <https://serokell.io/>
// SPDX-FileCopyrightText: 2021 Yannik Sander <contact@ysndr.de>
//
// SPDX-License-Identifier: MPL-2.0

use rnix::{types::*, SyntaxKind::*};
use merge::Merge;
use serde::Deserialize;
use std::collections::HashMap;
use thiserror::Error;

#[derive(Deserialize, Debug, Clone, Merge)]
pub struct GenericSettings {
#[serde(rename(deserialize = "sshUser"))]
use crate::settings;

#[derive(PartialEq, Debug)]
pub struct Target {
pub repo: String,
pub node: Option<String>,
pub profile: Option<String>,
}

#[derive(Error, Debug)]
pub enum ParseTargetError {
#[error("The given path was too long, did you mean to put something in quotes?")]
PathTooLong,
#[error("Unrecognized node or token encountered")]
Unrecognized,
}
impl std::str::FromStr for Target {
type Err = ParseTargetError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let flake_fragment_start = s.find('#');
let (repo, maybe_fragment) = match flake_fragment_start {
Some(i) => (s[..i].to_string(), Some(&s[i + 1..])),
None => (s.to_string(), None),
};

let mut node: Option<String> = None;
let mut profile: Option<String> = None;

if let Some(fragment) = maybe_fragment {
let ast = rnix::parse(fragment);

let first_child = match ast.root().node().first_child() {
Some(x) => x,
None => {
return Ok(Target {
repo,
node: None,
profile: None,
})
}
};

let mut node_over = false;

for entry in first_child.children_with_tokens() {
let x: Option<String> = match (entry.kind(), node_over) {
(TOKEN_DOT, false) => {
node_over = true;
None
}
(TOKEN_DOT, true) => {
return Err(ParseTargetError::PathTooLong);
}
(NODE_IDENT, _) => Some(entry.into_node().unwrap().text().to_string()),
(TOKEN_IDENT, _) => Some(entry.into_token().unwrap().text().to_string()),
(NODE_STRING, _) => {
let c = entry
.into_node()
.unwrap()
.children_with_tokens()
.nth(1)
.unwrap();

Some(c.into_token().unwrap().text().to_string())
}
_ => return Err(ParseTargetError::Unrecognized),
};

if !node_over {
node = x;
} else {
profile = x;
}
}
}

Ok(Target {
repo,
node,
profile,
})
}
}

#[test]
fn test_deploy_target_from_str() {
assert_eq!(
"../deploy/examples/system".parse::<Target>().unwrap(),
Target {
repo: "../deploy/examples/system".to_string(),
node: None,
profile: None,
}
);

assert_eq!(
"../deploy/examples/system#".parse::<Target>().unwrap(),
Target {
repo: "../deploy/examples/system".to_string(),
node: None,
profile: None,
}
);

assert_eq!(
"../deploy/examples/system#computer.\"something.nix\"".parse::<Target>().unwrap(),
Target {
repo: "../deploy/examples/system".to_string(),
node: Some("computer".to_string()),
profile: Some("something.nix".to_string()),
}
);

assert_eq!(
"../deploy/examples/system#\"example.com\".system".parse::<Target>().unwrap(),
Target {
repo: "../deploy/examples/system".to_string(),
node: Some("example.com".to_string()),
profile: Some("system".to_string()),
}
);

assert_eq!(
"../deploy/examples/system#example".parse::<Target>().unwrap(),
Target {
repo: "../deploy/examples/system".to_string(),
node: Some("example".to_string()),
profile: None
}
);

assert_eq!(
"../deploy/examples/system#example.system".parse::<Target>().unwrap(),
Target {
repo: "../deploy/examples/system".to_string(),
node: Some("example".to_string()),
profile: Some("system".to_string())
}
);

assert_eq!(
"../deploy/examples/system".parse::<Target>().unwrap(),
Target {
repo: "../deploy/examples/system".to_string(),
node: None,
profile: None,
}
);
}

#[derive(Debug)]
pub struct CmdOverrides {
pub ssh_user: Option<String>,
pub user: Option<String>,
#[serde(
skip_serializing_if = "Vec::is_empty",
default,
rename(deserialize = "sshOpts")
)]
#[merge(strategy = merge::vec::append)]
pub ssh_opts: Vec<String>,
#[serde(rename(deserialize = "fastConnection"))]
pub profile_user: Option<String>,
pub ssh_opts: Option<String>,
pub fast_connection: Option<bool>,
#[serde(rename(deserialize = "autoRollback"))]
pub auto_rollback: Option<bool>,
#[serde(rename(deserialize = "confirmTimeout"))]
pub confirm_timeout: Option<u16>,
#[serde(rename(deserialize = "tempPath"))]
pub temp_path: Option<String>,
#[serde(rename(deserialize = "magicRollback"))]
pub hostname: Option<String>,
pub magic_rollback: Option<bool>,
pub temp_path: Option<String>,
pub confirm_timeout: Option<u16>,
pub dry_activate: bool,
}

#[derive(Deserialize, Debug, Clone)]
pub struct NodeSettings {
pub hostname: String,
pub profiles: HashMap<String, Profile>,
#[serde(
skip_serializing_if = "Vec::is_empty",
default,
rename(deserialize = "profilesOrder")
)]
pub profiles_order: Vec<String>,
#[derive(Debug, Clone)]
pub struct DeployData<'a> {
pub node_name: &'a str,
pub node: &'a settings::Node,
pub profile_name: &'a str,
pub profile: &'a settings::Profile,

pub cmd_overrides: &'a CmdOverrides,

pub merged_settings: settings::GenericSettings,

pub debug_logs: bool,
pub log_dir: Option<&'a str>,
}

#[derive(Deserialize, Debug, Clone)]
pub struct ProfileSettings {
pub path: String,
#[serde(rename(deserialize = "profilePath"))]
pub profile_path: Option<String>,
#[derive(Debug)]
pub struct DeployDefs {
pub ssh_user: String,
pub profile_user: String,
pub profile_path: String,
pub sudo: Option<String>,
}

#[derive(Deserialize, Debug, Clone)]
pub struct Profile {
#[serde(flatten)]
pub profile_settings: ProfileSettings,
#[serde(flatten)]
pub generic_settings: GenericSettings,
#[derive(Error, Debug)]
pub enum DeployDataDefsError {
#[error("Neither `user` nor `sshUser` are set for profile {0} of node {1}")]
NoProfileUser(String, String),
}

#[derive(Deserialize, Debug, Clone)]
pub struct Node {
#[serde(flatten)]
pub generic_settings: GenericSettings,
#[serde(flatten)]
pub node_settings: NodeSettings,
impl<'a> DeployData<'a> {
pub fn defs(&'a self) -> Result<DeployDefs, DeployDataDefsError> {
let ssh_user = match self.merged_settings.ssh_user {
Some(ref u) => u.clone(),
None => whoami::username(),
};

let profile_user = self.get_profile_user()?;

let profile_path = self.get_profile_path()?;

let sudo: Option<String> = match self.merged_settings.user {
Some(ref user) if user != &ssh_user => Some(format!("sudo -u {}", user)),
_ => None,
};

Ok(DeployDefs {
ssh_user,
profile_user,
profile_path,
sudo,
})
}

pub fn get_profile_path(&'a self) -> Result<String, DeployDataDefsError> {
let profile_user = self.get_profile_user()?;
let profile_path = match self.profile.profile_settings.profile_path {
None => match &profile_user[..] {
"root" => format!("/nix/var/nix/profiles/{}", self.profile_name),
_ => format!(
"/nix/var/nix/profiles/per-user/{}/{}",
profile_user, self.profile_name
),
},
Some(ref x) => x.clone(),
};
Ok(profile_path)
}

pub fn get_profile_user(&'a self) -> Result<String, DeployDataDefsError> {
let profile_user = match self.merged_settings.user {
Some(ref x) => x.clone(),
None => match self.merged_settings.ssh_user {
Some(ref x) => x.clone(),
None => {
return Err(DeployDataDefsError::NoProfileUser(
self.profile_name.to_owned(),
self.node_name.to_owned(),
))
}
},
};
Ok(profile_user)
}
}

#[derive(Deserialize, Debug, Clone)]
pub struct Data {
#[serde(flatten)]
pub generic_settings: GenericSettings,
pub nodes: HashMap<String, Node>,
pub fn make_deploy_data<'a, 's>(
top_settings: &'s settings::GenericSettings,
node: &'a settings::Node,
node_name: &'a str,
profile: &'a settings::Profile,
profile_name: &'a str,
cmd_overrides: &'a CmdOverrides,
debug_logs: bool,
log_dir: Option<&'a str>,
) -> DeployData<'a> {
let mut merged_settings = profile.generic_settings.clone();
merged_settings.merge(node.generic_settings.clone());
merged_settings.merge(top_settings.clone());

if cmd_overrides.ssh_user.is_some() {
merged_settings.ssh_user = cmd_overrides.ssh_user.clone();
}
if cmd_overrides.profile_user.is_some() {
merged_settings.user = cmd_overrides.profile_user.clone();
}
if let Some(ref ssh_opts) = cmd_overrides.ssh_opts {
merged_settings.ssh_opts = ssh_opts.split(' ').map(|x| x.to_owned()).collect();
}
if let Some(fast_connection) = cmd_overrides.fast_connection {
merged_settings.fast_connection = Some(fast_connection);
}
if let Some(auto_rollback) = cmd_overrides.auto_rollback {
merged_settings.auto_rollback = Some(auto_rollback);
}
if let Some(magic_rollback) = cmd_overrides.magic_rollback {
merged_settings.magic_rollback = Some(magic_rollback);
}

DeployData {
node_name,
node,
profile_name,
profile,
cmd_overrides,
merged_settings,
debug_logs,
log_dir,
}
}
16 changes: 8 additions & 8 deletions src/deploy.rs
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ use std::borrow::Cow;
use thiserror::Error;
use tokio::process::Command;

use crate::DeployDataDefsError;
use crate::data;

struct ActivateCommandData<'a> {
sudo: &'a Option<String>,
@@ -207,8 +207,8 @@ pub enum ConfirmProfileError {
}

pub async fn confirm_profile(
deploy_data: &super::DeployData<'_>,
deploy_defs: &super::DeployDefs,
deploy_data: &data::DeployData<'_>,
deploy_defs: &data::DeployDefs,
temp_path: Cow<'_, str>,
ssh_addr: &str,
) -> Result<(), ConfirmProfileError> {
@@ -267,8 +267,8 @@ pub enum DeployProfileError {
}

pub async fn deploy_profile(
deploy_data: &super::DeployData<'_>,
deploy_defs: &super::DeployDefs,
deploy_data: &data::DeployData<'_>,
deploy_defs: &data::DeployDefs,
dry_activate: bool,
) -> Result<(), DeployProfileError> {
if !dry_activate {
@@ -415,11 +415,11 @@ pub enum RevokeProfileError {
SSHRevokeExit(Option<i32>),

#[error("Deployment data invalid: {0}")]
InvalidDeployDataDefs(#[from] DeployDataDefsError),
InvalidDeployDataDefs(#[from] data::DeployDataDefsError),
}
pub async fn revoke(
deploy_data: &crate::DeployData<'_>,
deploy_defs: &crate::DeployDefs,
deploy_data: &data::DeployData<'_>,
deploy_defs: &data::DeployDefs,
) -> Result<(), RevokeProfileError> {
let self_revoke_command = build_revoke_command(&RevokeCommandData {
sudo: &deploy_defs.sudo,
296 changes: 1 addition & 295 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -4,12 +4,6 @@
//
// SPDX-License-Identifier: MPL-2.0

use rnix::{types::*, SyntaxKind::*};

use merge::Merge;

use thiserror::Error;

use flexi_logger::*;

pub fn make_lock_path(temp_path: &str, closure: &str) -> String {
@@ -145,296 +139,8 @@ pub fn init_logger(
Ok(())
}

pub mod settings;
pub mod data;
pub mod deploy;
pub mod push;
pub mod cli;

#[derive(Debug)]
pub struct CmdOverrides {
pub ssh_user: Option<String>,
pub profile_user: Option<String>,
pub ssh_opts: Option<String>,
pub fast_connection: Option<bool>,
pub auto_rollback: Option<bool>,
pub hostname: Option<String>,
pub magic_rollback: Option<bool>,
pub temp_path: Option<String>,
pub confirm_timeout: Option<u16>,
pub dry_activate: bool,
}

#[derive(PartialEq, Debug)]
pub struct DeployFlake<'a> {
pub repo: &'a str,
pub node: Option<String>,
pub profile: Option<String>,
}

#[derive(Error, Debug)]
pub enum ParseFlakeError {
#[error("The given path was too long, did you mean to put something in quotes?")]
PathTooLong,
#[error("Unrecognized node or token encountered")]
Unrecognized,
}
pub fn parse_flake(flake: &str) -> Result<DeployFlake, ParseFlakeError> {
let flake_fragment_start = flake.find('#');
let (repo, maybe_fragment) = match flake_fragment_start {
Some(s) => (&flake[..s], Some(&flake[s + 1..])),
None => (flake, None),
};

let mut node: Option<String> = None;
let mut profile: Option<String> = None;

if let Some(fragment) = maybe_fragment {
let ast = rnix::parse(fragment);

let first_child = match ast.root().node().first_child() {
Some(x) => x,
None => {
return Ok(DeployFlake {
repo,
node: None,
profile: None,
})
}
};

let mut node_over = false;

for entry in first_child.children_with_tokens() {
let x: Option<String> = match (entry.kind(), node_over) {
(TOKEN_DOT, false) => {
node_over = true;
None
}
(TOKEN_DOT, true) => {
return Err(ParseFlakeError::PathTooLong);
}
(NODE_IDENT, _) => Some(entry.into_node().unwrap().text().to_string()),
(TOKEN_IDENT, _) => Some(entry.into_token().unwrap().text().to_string()),
(NODE_STRING, _) => {
let c = entry
.into_node()
.unwrap()
.children_with_tokens()
.nth(1)
.unwrap();

Some(c.into_token().unwrap().text().to_string())
}
_ => return Err(ParseFlakeError::Unrecognized),
};

if !node_over {
node = x;
} else {
profile = x;
}
}
}

Ok(DeployFlake {
repo,
node,
profile,
})
}

#[test]
fn test_parse_flake() {
assert_eq!(
parse_flake("../deploy/examples/system").unwrap(),
DeployFlake {
repo: "../deploy/examples/system",
node: None,
profile: None,
}
);

assert_eq!(
parse_flake("../deploy/examples/system#").unwrap(),
DeployFlake {
repo: "../deploy/examples/system",
node: None,
profile: None,
}
);

assert_eq!(
parse_flake("../deploy/examples/system#computer.\"something.nix\"").unwrap(),
DeployFlake {
repo: "../deploy/examples/system",
node: Some("computer".to_string()),
profile: Some("something.nix".to_string()),
}
);

assert_eq!(
parse_flake("../deploy/examples/system#\"example.com\".system").unwrap(),
DeployFlake {
repo: "../deploy/examples/system",
node: Some("example.com".to_string()),
profile: Some("system".to_string()),
}
);

assert_eq!(
parse_flake("../deploy/examples/system#example").unwrap(),
DeployFlake {
repo: "../deploy/examples/system",
node: Some("example".to_string()),
profile: None
}
);

assert_eq!(
parse_flake("../deploy/examples/system#example.system").unwrap(),
DeployFlake {
repo: "../deploy/examples/system",
node: Some("example".to_string()),
profile: Some("system".to_string())
}
);

assert_eq!(
parse_flake("../deploy/examples/system").unwrap(),
DeployFlake {
repo: "../deploy/examples/system",
node: None,
profile: None,
}
);
}

#[derive(Debug, Clone)]
pub struct DeployData<'a> {
pub node_name: &'a str,
pub node: &'a data::Node,
pub profile_name: &'a str,
pub profile: &'a data::Profile,

pub cmd_overrides: &'a CmdOverrides,

pub merged_settings: data::GenericSettings,

pub debug_logs: bool,
pub log_dir: Option<&'a str>,
}

#[derive(Debug)]
pub struct DeployDefs {
pub ssh_user: String,
pub profile_user: String,
pub profile_path: String,
pub sudo: Option<String>,
}

#[derive(Error, Debug)]
pub enum DeployDataDefsError {
#[error("Neither `user` nor `sshUser` are set for profile {0} of node {1}")]
NoProfileUser(String, String),
}

impl<'a> DeployData<'a> {
pub fn defs(&'a self) -> Result<DeployDefs, DeployDataDefsError> {
let ssh_user = match self.merged_settings.ssh_user {
Some(ref u) => u.clone(),
None => whoami::username(),
};

let profile_user = self.get_profile_user()?;

let profile_path = self.get_profile_path()?;

let sudo: Option<String> = match self.merged_settings.user {
Some(ref user) if user != &ssh_user => Some(format!("sudo -u {}", user)),
_ => None,
};

Ok(DeployDefs {
ssh_user,
profile_user,
profile_path,
sudo,
})
}

fn get_profile_path(&'a self) -> Result<String, DeployDataDefsError> {
let profile_user = self.get_profile_user()?;
let profile_path = match self.profile.profile_settings.profile_path {
None => match &profile_user[..] {
"root" => format!("/nix/var/nix/profiles/{}", self.profile_name),
_ => format!(
"/nix/var/nix/profiles/per-user/{}/{}",
profile_user, self.profile_name
),
},
Some(ref x) => x.clone(),
};
Ok(profile_path)
}

fn get_profile_user(&'a self) -> Result<String, DeployDataDefsError> {
let profile_user = match self.merged_settings.user {
Some(ref x) => x.clone(),
None => match self.merged_settings.ssh_user {
Some(ref x) => x.clone(),
None => {
return Err(DeployDataDefsError::NoProfileUser(
self.profile_name.to_owned(),
self.node_name.to_owned(),
))
}
},
};
Ok(profile_user)
}
}

pub fn make_deploy_data<'a, 's>(
top_settings: &'s data::GenericSettings,
node: &'a data::Node,
node_name: &'a str,
profile: &'a data::Profile,
profile_name: &'a str,
cmd_overrides: &'a CmdOverrides,
debug_logs: bool,
log_dir: Option<&'a str>,
) -> DeployData<'a> {
let mut merged_settings = profile.generic_settings.clone();
merged_settings.merge(node.generic_settings.clone());
merged_settings.merge(top_settings.clone());

if cmd_overrides.ssh_user.is_some() {
merged_settings.ssh_user = cmd_overrides.ssh_user.clone();
}
if cmd_overrides.profile_user.is_some() {
merged_settings.user = cmd_overrides.profile_user.clone();
}
if let Some(ref ssh_opts) = cmd_overrides.ssh_opts {
merged_settings.ssh_opts = ssh_opts.split(' ').map(|x| x.to_owned()).collect();
}
if let Some(fast_connection) = cmd_overrides.fast_connection {
merged_settings.fast_connection = Some(fast_connection);
}
if let Some(auto_rollback) = cmd_overrides.auto_rollback {
merged_settings.auto_rollback = Some(auto_rollback);
}
if let Some(magic_rollback) = cmd_overrides.magic_rollback {
merged_settings.magic_rollback = Some(magic_rollback);
}

DeployData {
node_name,
node,
profile_name,
profile,
cmd_overrides,
merged_settings,
debug_logs,
log_dir,
}
}
6 changes: 4 additions & 2 deletions src/push.rs
Original file line number Diff line number Diff line change
@@ -9,6 +9,8 @@ use std::process::Stdio;
use thiserror::Error;
use tokio::process::Command;

use crate::data;

#[derive(Error, Debug)]
pub enum PushProfileError {
#[error("Failed to run Nix show-derivation command: {0}")]
@@ -47,8 +49,8 @@ pub struct PushProfileData<'a> {
pub supports_flakes: bool,
pub check_sigs: bool,
pub repo: &'a str,
pub deploy_data: &'a super::DeployData<'a>,
pub deploy_defs: &'a super::DeployDefs,
pub deploy_data: &'a data::DeployData<'a>,
pub deploy_defs: &'a data::DeployDefs,
pub keep_result: bool,
pub result_path: Option<&'a str>,
pub extra_build_args: &'a [String],
73 changes: 73 additions & 0 deletions src/settings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-FileCopyrightText: 2020 Serokell <https://serokell.io/>
//
// SPDX-License-Identifier: MPL-2.0

use merge::Merge;
use serde::Deserialize;
use std::collections::HashMap;

#[derive(Deserialize, Debug, Clone, Merge)]
pub struct GenericSettings {
#[serde(rename(deserialize = "sshUser"))]
pub ssh_user: Option<String>,
pub user: Option<String>,
#[serde(
skip_serializing_if = "Vec::is_empty",
default,
rename(deserialize = "sshOpts")
)]
#[merge(strategy = merge::vec::append)]
pub ssh_opts: Vec<String>,
#[serde(rename(deserialize = "fastConnection"))]
pub fast_connection: Option<bool>,
#[serde(rename(deserialize = "autoRollback"))]
pub auto_rollback: Option<bool>,
#[serde(rename(deserialize = "confirmTimeout"))]
pub confirm_timeout: Option<u16>,
#[serde(rename(deserialize = "tempPath"))]
pub temp_path: Option<String>,
#[serde(rename(deserialize = "magicRollback"))]
pub magic_rollback: Option<bool>,
}

#[derive(Deserialize, Debug, Clone)]
pub struct NodeSettings {
pub hostname: String,
pub profiles: HashMap<String, Profile>,
#[serde(
skip_serializing_if = "Vec::is_empty",
default,
rename(deserialize = "profilesOrder")
)]
pub profiles_order: Vec<String>,
}

#[derive(Deserialize, Debug, Clone)]
pub struct ProfileSettings {
pub path: String,
#[serde(rename(deserialize = "profilePath"))]
pub profile_path: Option<String>,
}

#[derive(Deserialize, Debug, Clone)]
pub struct Profile {
#[serde(flatten)]
pub profile_settings: ProfileSettings,
#[serde(flatten)]
pub generic_settings: GenericSettings,
}

#[derive(Deserialize, Debug, Clone)]
pub struct Node {
#[serde(flatten)]
pub generic_settings: GenericSettings,
#[serde(flatten)]
pub node_settings: NodeSettings,
}

#[derive(Deserialize, Debug, Clone)]
pub struct Root {
#[serde(flatten)]
pub generic_settings: GenericSettings,
pub nodes: HashMap<String, Node>,
}