2020-12-15 20:21:26 -08:00
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use std::convert::AsRef;
|
|
|
|
use std::io::Write;
|
2020-12-18 01:27:44 -08:00
|
|
|
use std::process::Stdio;
|
2020-12-15 20:21:26 -08:00
|
|
|
use std::collections::HashMap;
|
|
|
|
use std::fs;
|
|
|
|
|
|
|
|
use async_trait::async_trait;
|
2020-12-18 01:27:44 -08:00
|
|
|
use indicatif::ProgressBar;
|
2020-12-15 20:21:26 -08:00
|
|
|
use serde::de::DeserializeOwned;
|
|
|
|
use serde::{Serialize, Deserialize};
|
|
|
|
use snafu::Snafu;
|
|
|
|
use tempfile::{NamedTempFile, TempPath};
|
|
|
|
use tokio::process::Command;
|
2020-12-18 01:27:44 -08:00
|
|
|
use tokio::sync::Mutex;
|
|
|
|
|
2020-12-19 15:07:29 -08:00
|
|
|
pub mod host;
|
2020-12-18 01:27:44 -08:00
|
|
|
pub use host::{Host, CopyDirection};
|
|
|
|
use host::SSH;
|
2020-12-15 20:21:26 -08:00
|
|
|
|
|
|
|
const HIVE_EVAL: &'static [u8] = include_bytes!("eval.nix");
|
|
|
|
|
2020-12-19 16:28:34 -08:00
|
|
|
pub const SYSTEM_PROFILE: &'static str = "/nix/var/nix/profiles/system";
|
|
|
|
|
2020-12-18 01:27:44 -08:00
|
|
|
pub type NixResult<T> = Result<T, NixError>;
|
|
|
|
|
|
|
|
#[derive(Debug, Snafu)]
|
|
|
|
pub enum NixError {
|
|
|
|
#[snafu(display("I/O Error: {}", error))]
|
|
|
|
IoError { error: std::io::Error },
|
|
|
|
|
|
|
|
#[snafu(display("Nix returned invalid response: {}", output))]
|
|
|
|
BadOutput { output: String },
|
|
|
|
|
|
|
|
#[snafu(display("Nix exited with error code: {}", exit_code))]
|
|
|
|
NixFailure { exit_code: i32 },
|
|
|
|
|
|
|
|
#[snafu(display("Nix was interrupted"))]
|
|
|
|
NixKilled,
|
|
|
|
|
|
|
|
#[snafu(display("This operation is not supported"))]
|
|
|
|
Unsupported,
|
|
|
|
|
|
|
|
#[snafu(display("Nix Error: {}", message))]
|
|
|
|
Unknown { message: String },
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<std::io::Error> for NixError {
|
|
|
|
fn from(error: std::io::Error) -> Self {
|
|
|
|
Self::IoError { error }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct Hive {
|
|
|
|
hive: PathBuf,
|
|
|
|
eval_nix: TempPath,
|
|
|
|
builder: Box<dyn Host>,
|
2020-12-28 21:35:43 -08:00
|
|
|
show_trace: bool,
|
2020-12-18 01:27:44 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Hive {
|
|
|
|
pub fn new<P: AsRef<Path>>(hive: P) -> NixResult<Self> {
|
|
|
|
let mut eval_nix = NamedTempFile::new()?;
|
|
|
|
eval_nix.write_all(HIVE_EVAL)?;
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
hive: hive.as_ref().to_owned(),
|
|
|
|
eval_nix: eval_nix.into_temp_path(),
|
|
|
|
builder: host::local(),
|
2020-12-28 21:35:43 -08:00
|
|
|
show_trace: false,
|
2020-12-18 01:27:44 -08:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-12-29 11:31:19 -08:00
|
|
|
pub fn show_trace(&mut self, value: bool) {
|
|
|
|
self.show_trace = value;
|
2020-12-18 01:27:44 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Retrieve deployment info for all nodes
|
|
|
|
pub async fn deployment_info(&self) -> NixResult<HashMap<String, DeploymentConfig>> {
|
|
|
|
// FIXME: Really ugly :(
|
|
|
|
let s: String = self.nix_instantiate("hive.deploymentConfigJson").eval()
|
|
|
|
.capture_json().await?;
|
|
|
|
|
|
|
|
Ok(serde_json::from_str(&s).unwrap())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Builds selected nodes
|
|
|
|
pub async fn build_selected(&mut self, nodes: Vec<String>) -> NixResult<HashMap<String, StorePath>> {
|
|
|
|
let nodes_expr = SerializedNixExpresssion::new(&nodes)?;
|
|
|
|
let expr = format!("hive.buildSelected {{ names = {}; }}", nodes_expr.expression());
|
|
|
|
|
|
|
|
self.build_common(&expr).await
|
|
|
|
}
|
|
|
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
/// Builds all node configurations
|
|
|
|
pub async fn build_all(&mut self) -> NixResult<HashMap<String, StorePath>> {
|
|
|
|
self.build_common("hive.buildAll").await
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Evaluates an expression using values from the configuration
|
|
|
|
pub async fn introspect(&mut self, expression: String) -> NixResult<String> {
|
|
|
|
let expression = format!("toJSON (hive.introspect ({}))", expression);
|
|
|
|
self.nix_instantiate(&expression).eval()
|
|
|
|
.capture_json().await
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Builds node configurations
|
|
|
|
///
|
|
|
|
/// Expects the resulting store path to point to a JSON file containing
|
|
|
|
/// a map of node name -> store path.
|
|
|
|
async fn build_common(&mut self, expression: &str) -> NixResult<HashMap<String, StorePath>> {
|
|
|
|
let build: StorePath = self.nix_instantiate(expression).instantiate()
|
|
|
|
.capture_store_path().await?;
|
|
|
|
|
|
|
|
let realization = self.builder.realize(&build).await?;
|
|
|
|
assert!(realization.len() == 1);
|
|
|
|
|
|
|
|
let json = fs::read_to_string(&realization[0].as_path())?;
|
|
|
|
let result_map = serde_json::from_str(&json)
|
|
|
|
.expect("Bad result from our own build routine");
|
|
|
|
|
|
|
|
Ok(result_map)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn nix_instantiate(&self, expression: &str) -> NixInstantiate {
|
2020-12-28 21:35:43 -08:00
|
|
|
NixInstantiate::new(&self, expression.to_owned())
|
2020-12-18 01:27:44 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-15 20:21:26 -08:00
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
2020-12-18 01:27:44 -08:00
|
|
|
pub struct DeploymentConfig {
|
2020-12-15 20:21:26 -08:00
|
|
|
#[serde(rename = "targetHost")]
|
2020-12-19 15:07:29 -08:00
|
|
|
target_host: Option<String>,
|
2020-12-15 20:21:26 -08:00
|
|
|
|
|
|
|
#[serde(rename = "targetUser")]
|
|
|
|
target_user: String,
|
2020-12-19 15:07:29 -08:00
|
|
|
|
|
|
|
#[serde(rename = "allowLocalDeployment")]
|
|
|
|
allow_local_deployment: bool,
|
2020-12-15 20:21:26 -08:00
|
|
|
tags: Vec<String>,
|
|
|
|
}
|
|
|
|
|
2020-12-18 01:27:44 -08:00
|
|
|
impl DeploymentConfig {
|
|
|
|
pub fn tags(&self) -> &[String] { &self.tags }
|
2020-12-19 15:07:29 -08:00
|
|
|
pub fn allows_local_deployment(&self) -> bool { self.allow_local_deployment }
|
|
|
|
|
|
|
|
pub fn to_ssh_host(&self) -> Option<Box<dyn Host>> {
|
|
|
|
self.target_host.as_ref().map(|target_host| {
|
|
|
|
let host = SSH::new(self.target_user.clone(), target_host.clone());
|
|
|
|
let host: Box<dyn Host> = Box::new(host);
|
|
|
|
host
|
|
|
|
})
|
2020-12-18 01:27:44 -08:00
|
|
|
}
|
2020-12-15 20:21:26 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Copy, Clone)]
|
|
|
|
pub enum DeploymentGoal {
|
|
|
|
/// Push the closures only.
|
|
|
|
Push,
|
|
|
|
|
|
|
|
/// Make the configuration the boot default and activate now.
|
|
|
|
Switch,
|
|
|
|
|
|
|
|
/// Make the configuration the boot default.
|
|
|
|
Boot,
|
|
|
|
|
|
|
|
/// Activate the configuration, but don't make it the boot default.
|
|
|
|
Test,
|
|
|
|
|
|
|
|
/// Show what would be done if this configuration were activated.
|
|
|
|
DryActivate,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl DeploymentGoal {
|
|
|
|
pub fn from_str(s: &str) -> Option<Self> {
|
|
|
|
match s {
|
|
|
|
"push" => Some(Self::Push),
|
|
|
|
"switch" => Some(Self::Switch),
|
|
|
|
"boot" => Some(Self::Boot),
|
|
|
|
"test" => Some(Self::Test),
|
|
|
|
"dry-activate" => Some(Self::DryActivate),
|
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn as_str(&self) -> Option<&'static str> {
|
|
|
|
use DeploymentGoal::*;
|
|
|
|
match self {
|
|
|
|
Push => None,
|
|
|
|
Switch => Some("switch"),
|
|
|
|
Boot => Some("boot"),
|
|
|
|
Test => Some("test"),
|
|
|
|
DryActivate => Some("dry-activate"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn success_str(&self) -> Option<&'static str> {
|
|
|
|
use DeploymentGoal::*;
|
|
|
|
match self {
|
|
|
|
Push => Some("Pushed"),
|
|
|
|
Switch => Some("Activation successful"),
|
|
|
|
Boot => Some("Will be activated next boot"),
|
|
|
|
Test => Some("Activation successful (test)"),
|
|
|
|
DryActivate => Some("Dry activation successful"),
|
|
|
|
}
|
|
|
|
}
|
2020-12-19 16:34:24 -08:00
|
|
|
|
|
|
|
pub fn should_switch_profile(&self) -> bool {
|
|
|
|
use DeploymentGoal::*;
|
|
|
|
match self {
|
|
|
|
Boot | Switch => true,
|
|
|
|
_ => false,
|
|
|
|
}
|
|
|
|
}
|
2020-12-15 20:21:26 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
struct NixInstantiate<'hive> {
|
2020-12-28 21:35:43 -08:00
|
|
|
hive: &'hive Hive,
|
2020-12-15 20:21:26 -08:00
|
|
|
expression: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'hive> NixInstantiate<'hive> {
|
2020-12-28 21:35:43 -08:00
|
|
|
fn new(hive: &'hive Hive, expression: String) -> Self {
|
2020-12-15 20:21:26 -08:00
|
|
|
Self {
|
|
|
|
hive,
|
2020-12-28 21:35:43 -08:00
|
|
|
expression,
|
2020-12-15 20:21:26 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn instantiate(self) -> Command {
|
|
|
|
// FIXME: unwrap
|
|
|
|
// Technically filenames can be arbitrary byte strings (OsStr),
|
|
|
|
// but Nix may not like it...
|
|
|
|
|
|
|
|
let mut command = Command::new("nix-instantiate");
|
|
|
|
command
|
2020-12-29 12:10:00 -08:00
|
|
|
.arg("--no-gc-warning")
|
2020-12-15 20:21:26 -08:00
|
|
|
.arg("-E")
|
|
|
|
.arg(format!(
|
|
|
|
"with builtins; let eval = import {}; hive = eval {{ rawHive = import {}; }}; in {}",
|
2020-12-28 21:35:43 -08:00
|
|
|
self.hive.eval_nix.to_str().unwrap(),
|
|
|
|
self.hive.hive.to_str().unwrap(),
|
2020-12-15 20:21:26 -08:00
|
|
|
self.expression,
|
|
|
|
));
|
2020-12-28 21:35:43 -08:00
|
|
|
|
|
|
|
if self.hive.show_trace {
|
|
|
|
command.arg("--show-trace");
|
|
|
|
}
|
|
|
|
|
2020-12-15 20:21:26 -08:00
|
|
|
command
|
|
|
|
}
|
|
|
|
|
|
|
|
fn eval(self) -> Command {
|
|
|
|
let mut command = self.instantiate();
|
|
|
|
command.arg("--eval").arg("--json");
|
|
|
|
command
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[async_trait]
|
|
|
|
trait NixCommand {
|
|
|
|
async fn passthrough(&mut self) -> NixResult<()>;
|
|
|
|
async fn capture_output(&mut self) -> NixResult<String>;
|
|
|
|
async fn capture_json<T>(&mut self) -> NixResult<T> where T: DeserializeOwned;
|
|
|
|
async fn capture_store_path(&mut self) -> NixResult<StorePath>;
|
|
|
|
}
|
|
|
|
|
|
|
|
#[async_trait]
|
|
|
|
impl NixCommand for Command {
|
|
|
|
/// Runs the command with stdout and stderr passed through to the user.
|
|
|
|
async fn passthrough(&mut self) -> NixResult<()> {
|
|
|
|
let exit = self
|
2020-12-18 01:27:44 -08:00
|
|
|
.spawn()?
|
2020-12-15 20:21:26 -08:00
|
|
|
.wait()
|
2020-12-18 01:27:44 -08:00
|
|
|
.await?;
|
2020-12-15 20:21:26 -08:00
|
|
|
|
|
|
|
if exit.success() {
|
|
|
|
Ok(())
|
|
|
|
} else {
|
|
|
|
Err(match exit.code() {
|
|
|
|
Some(exit_code) => NixError::NixFailure { exit_code },
|
|
|
|
None => NixError::NixKilled,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Captures output as a String.
|
|
|
|
async fn capture_output(&mut self) -> NixResult<String> {
|
|
|
|
// We want the user to see the raw errors
|
|
|
|
let output = self
|
|
|
|
.stdout(Stdio::piped())
|
|
|
|
.stderr(Stdio::inherit())
|
2020-12-18 01:27:44 -08:00
|
|
|
.spawn()?
|
2020-12-15 20:21:26 -08:00
|
|
|
.wait_with_output()
|
2020-12-18 01:27:44 -08:00
|
|
|
.await?;
|
2020-12-15 20:21:26 -08:00
|
|
|
|
|
|
|
if output.status.success() {
|
|
|
|
// FIXME: unwrap
|
|
|
|
Ok(String::from_utf8(output.stdout).unwrap())
|
|
|
|
} else {
|
|
|
|
Err(match output.status.code() {
|
|
|
|
Some(exit_code) => NixError::NixFailure { exit_code },
|
|
|
|
None => NixError::NixKilled,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Captures deserialized output from JSON.
|
|
|
|
async fn capture_json<T>(&mut self) -> NixResult<T> where T: DeserializeOwned {
|
|
|
|
let output = self.capture_output().await?;
|
|
|
|
serde_json::from_str(&output).map_err(|_| NixError::BadOutput {
|
|
|
|
output: output.clone()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Captures a single store path.
|
|
|
|
async fn capture_store_path(&mut self) -> NixResult<StorePath> {
|
|
|
|
let output = self.capture_output().await?;
|
|
|
|
Ok(StorePath(output.trim_end().into()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A Nix store path.
|
2020-12-18 01:27:44 -08:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
pub struct StorePath(PathBuf);
|
2020-12-15 20:21:26 -08:00
|
|
|
|
|
|
|
impl StorePath {
|
2020-12-18 01:27:44 -08:00
|
|
|
/// Returns the store path
|
|
|
|
pub fn as_path(&self) -> &Path {
|
|
|
|
&self.0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<String> for StorePath {
|
|
|
|
fn from(s: String) -> Self {
|
|
|
|
Self(s.into())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Into<PathBuf> for StorePath {
|
|
|
|
fn into(self) -> PathBuf {
|
|
|
|
self.0
|
2020-12-15 20:21:26 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A serialized Nix expression.
|
|
|
|
///
|
|
|
|
/// Very hacky and involves an Import From Derivation, so should be
|
|
|
|
/// avoided as much as possible. But I suppose it's more robust than attempting
|
|
|
|
/// to generate Nix expressions directly or escaping a JSON string to strip
|
|
|
|
/// off Nix interpolation.
|
|
|
|
struct SerializedNixExpresssion {
|
|
|
|
json_file: TempPath,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl SerializedNixExpresssion {
|
|
|
|
pub fn new<'de, T>(data: T) -> NixResult<Self> where T: Serialize {
|
2020-12-18 01:27:44 -08:00
|
|
|
let mut tmp = NamedTempFile::new()?;
|
2020-12-15 20:21:26 -08:00
|
|
|
let json = serde_json::to_vec(&data).expect("Could not serialize data");
|
2020-12-18 01:27:44 -08:00
|
|
|
tmp.write_all(&json)?;
|
2020-12-15 20:21:26 -08:00
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
json_file: tmp.into_temp_path(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn expression(&self) -> String {
|
|
|
|
format!("(builtins.fromJSON (builtins.readFile {}))", self.json_file.to_str().unwrap())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-18 01:27:44 -08:00
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct DeploymentTask {
|
|
|
|
/// Name of the target.
|
|
|
|
name: String,
|
2020-12-15 20:21:26 -08:00
|
|
|
|
2020-12-18 01:27:44 -08:00
|
|
|
/// The target to deploy to.
|
|
|
|
target: Mutex<Box<dyn Host>>,
|
2020-12-15 20:21:26 -08:00
|
|
|
|
2020-12-18 01:27:44 -08:00
|
|
|
/// Nix store path to the system profile to deploy.
|
|
|
|
profile: StorePath,
|
2020-12-15 20:21:26 -08:00
|
|
|
|
2020-12-18 01:27:44 -08:00
|
|
|
/// The goal of this deployment.
|
|
|
|
goal: DeploymentGoal,
|
2020-12-15 20:21:26 -08:00
|
|
|
}
|
|
|
|
|
2020-12-18 01:27:44 -08:00
|
|
|
impl DeploymentTask {
|
|
|
|
pub fn new(name: String, target: Box<dyn Host>, profile: StorePath, goal: DeploymentGoal) -> Self {
|
2020-12-15 20:21:26 -08:00
|
|
|
Self {
|
|
|
|
name,
|
2020-12-18 01:27:44 -08:00
|
|
|
target: Mutex::new(target),
|
2020-12-15 20:21:26 -08:00
|
|
|
profile,
|
|
|
|
goal,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn name(&self) -> &str { &self.name }
|
|
|
|
pub fn goal(&self) -> DeploymentGoal { self.goal }
|
|
|
|
|
|
|
|
/// Set the progress bar used during deployment.
|
2020-12-18 01:27:44 -08:00
|
|
|
pub async fn set_progress_bar(&mut self, progress: ProgressBar) {
|
|
|
|
let mut target = self.target.lock().await;
|
|
|
|
target.set_progress_bar(progress);
|
2020-12-15 20:21:26 -08:00
|
|
|
}
|
|
|
|
|
2020-12-18 01:27:44 -08:00
|
|
|
/// Executes the deployment.
|
|
|
|
pub async fn execute(&mut self) -> NixResult<()> {
|
2020-12-15 20:21:26 -08:00
|
|
|
match self.goal {
|
|
|
|
DeploymentGoal::Push => {
|
|
|
|
self.push().await
|
|
|
|
}
|
|
|
|
_ => {
|
|
|
|
self.push_and_activate().await
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-18 01:27:44 -08:00
|
|
|
/// Takes the Host out, consuming the DeploymentTask.
|
|
|
|
pub async fn to_host(self) -> Box<dyn Host> {
|
|
|
|
self.target.into_inner()
|
2020-12-15 20:21:26 -08:00
|
|
|
}
|
|
|
|
|
2020-12-18 01:27:44 -08:00
|
|
|
async fn push(&mut self) -> NixResult<()> {
|
|
|
|
let mut target = self.target.lock().await;
|
|
|
|
target.copy_closure(&self.profile, CopyDirection::ToRemote, true).await
|
2020-12-15 20:21:26 -08:00
|
|
|
}
|
|
|
|
|
2020-12-18 01:27:44 -08:00
|
|
|
async fn push_and_activate(&mut self) -> NixResult<()> {
|
|
|
|
self.push().await?;
|
|
|
|
{
|
|
|
|
let mut target = self.target.lock().await;
|
|
|
|
target.activate(&self.profile, self.goal).await
|
2020-12-15 20:21:26 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|