forked from DGNum/colmena
host: Use the key uploader script for both SSH and local
This commit is contained in:
parent
29cfd45141
commit
e9487ced9e
5 changed files with 77 additions and 94 deletions
59
src/nix/host/key_uploader.rs
Normal file
59
src/nix/host/key_uploader.rs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
//! Utilities for using the key uploader script.
|
||||||
|
//!
|
||||||
|
//! The key uploader is a simple shell script that reads the contents
|
||||||
|
//! of the secret file from stdin into a temporary file then atomically
|
||||||
|
//! replaces the destination file with the temporary file.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use futures::future::join3;
|
||||||
|
use shell_escape::unix::escape;
|
||||||
|
use tokio::io::{AsyncWriteExt, BufReader};
|
||||||
|
use tokio::process::Child;
|
||||||
|
|
||||||
|
use crate::nix::{Key, NixError, NixResult};
|
||||||
|
use crate::progress::TaskProgress;
|
||||||
|
use crate::util::capture_stream;
|
||||||
|
|
||||||
|
const SCRIPT_TEMPLATE: &'static str = include_str!("./key_uploader.template.sh");
|
||||||
|
|
||||||
|
pub fn generate_script<'a>(key: &'a Key, destination: &'a Path) -> Cow<'a, str> {
|
||||||
|
let key_script = SCRIPT_TEMPLATE.to_string()
|
||||||
|
.replace("%DESTINATION%", destination.to_str().unwrap())
|
||||||
|
.replace("%USER%", &escape(key.user().into()))
|
||||||
|
.replace("%GROUP%", &escape(key.group().into()))
|
||||||
|
.replace("%PERMISSIONS%", &escape(key.permissions().into()))
|
||||||
|
.trim_end_matches('\n').to_string();
|
||||||
|
|
||||||
|
escape(key_script.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn feed_uploader(mut uploader: Child, key: &Key, progress: TaskProgress, logs: &mut String) -> NixResult<()> {
|
||||||
|
let mut reader = key.reader().await?;
|
||||||
|
let mut stdin = uploader.stdin.take().unwrap();
|
||||||
|
|
||||||
|
tokio::io::copy(reader.as_mut(), &mut stdin).await?;
|
||||||
|
stdin.flush().await?;
|
||||||
|
drop(stdin);
|
||||||
|
|
||||||
|
let stdout = BufReader::new(uploader.stdout.take().unwrap());
|
||||||
|
let stderr = BufReader::new(uploader.stderr.take().unwrap());
|
||||||
|
|
||||||
|
let futures = join3(
|
||||||
|
capture_stream(stdout, progress.clone()),
|
||||||
|
capture_stream(stderr, progress.clone()),
|
||||||
|
uploader.wait(),
|
||||||
|
);
|
||||||
|
let (stdout_str, stderr_str, exit) = futures.await;
|
||||||
|
logs.push_str(&stdout_str);
|
||||||
|
logs.push_str(&stderr_str);
|
||||||
|
|
||||||
|
let exit = exit?;
|
||||||
|
|
||||||
|
if exit.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(NixError::NixFailure { exit_code: exit.code().unwrap() })
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,11 @@
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::process::Stdio;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use tokio::fs::OpenOptions;
|
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use tempfile::NamedTempFile;
|
|
||||||
|
|
||||||
use super::{CopyDirection, CopyOptions, Host};
|
use super::{CopyDirection, CopyOptions, Host, key_uploader};
|
||||||
use crate::nix::{StorePath, Profile, Goal, NixResult, NixCommand, Key, SYSTEM_PROFILE};
|
use crate::nix::{StorePath, Profile, Goal, NixResult, NixCommand, Key, SYSTEM_PROFILE};
|
||||||
use crate::util::CommandExecution;
|
use crate::util::CommandExecution;
|
||||||
use crate::progress::TaskProgress;
|
use crate::progress::TaskProgress;
|
||||||
|
@ -108,56 +106,16 @@ impl Local {
|
||||||
self.progress_bar.log(&format!("Deploying key {}", name));
|
self.progress_bar.log(&format!("Deploying key {}", name));
|
||||||
|
|
||||||
let dest_path = key.dest_dir().join(name);
|
let dest_path = key.dest_dir().join(name);
|
||||||
|
let key_script = format!("'{}'", key_uploader::generate_script(key, &dest_path));
|
||||||
|
|
||||||
let temp = NamedTempFile::new()?;
|
let mut command = Command::new("sh");
|
||||||
let (_, temp_path) = temp.keep().map_err(|pe| pe.error)?;
|
|
||||||
let mut reader = key.reader().await?;
|
|
||||||
let mut writer = OpenOptions::new().write(true).open(&temp_path).await?;
|
|
||||||
tokio::io::copy(reader.as_mut(), &mut writer).await?;
|
|
||||||
|
|
||||||
// Well, we need the userspace chmod program to parse the
|
command.args(&["-c", &key_script]);
|
||||||
// permission, for NixOps compatibility
|
command.stdin(Stdio::piped());
|
||||||
{
|
command.stderr(Stdio::piped());
|
||||||
let mut command = Command::new("chmod");
|
command.stdout(Stdio::piped());
|
||||||
command
|
|
||||||
.arg(&key.permissions())
|
|
||||||
.arg(&temp_path);
|
|
||||||
|
|
||||||
let mut execution = CommandExecution::new(command);
|
let uploader = command.spawn()?;
|
||||||
let exit = execution.run().await;
|
key_uploader::feed_uploader(uploader, key, self.progress_bar.clone(), &mut self.logs).await
|
||||||
|
|
||||||
let (stdout, stderr) = execution.get_logs();
|
|
||||||
self.logs += stdout.unwrap();
|
|
||||||
self.logs += stderr.unwrap();
|
|
||||||
|
|
||||||
exit?;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
let mut command = Command::new("chown");
|
|
||||||
command
|
|
||||||
.arg(&format!("{}:{}", key.user(), key.group()))
|
|
||||||
.arg(&temp_path);
|
|
||||||
|
|
||||||
let mut execution = CommandExecution::new(command);
|
|
||||||
let exit = execution.run().await;
|
|
||||||
|
|
||||||
let (stdout, stderr) = execution.get_logs();
|
|
||||||
self.logs += stdout.unwrap();
|
|
||||||
self.logs += stderr.unwrap();
|
|
||||||
|
|
||||||
exit?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let parent_dir = dest_path.parent().unwrap();
|
|
||||||
fs::create_dir_all(parent_dir)?;
|
|
||||||
|
|
||||||
if fs::rename(&temp_path, &dest_path).is_err() {
|
|
||||||
// Linux can not rename across different filesystems, try copy-then-remove
|
|
||||||
let copy_result = fs::copy(&temp_path, &dest_path);
|
|
||||||
fs::remove_file(&temp_path)?;
|
|
||||||
copy_result?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ pub use ssh::Ssh;
|
||||||
mod local;
|
mod local;
|
||||||
pub use local::Local;
|
pub use local::Local;
|
||||||
|
|
||||||
|
mod key_uploader;
|
||||||
|
|
||||||
pub(crate) fn local() -> Box<dyn Host + 'static> {
|
pub(crate) fn local() -> Box<dyn Host + 'static> {
|
||||||
Box::new(Local::new())
|
Box::new(Local::new())
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,18 +4,13 @@ use std::path::PathBuf;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use futures::future::join3;
|
|
||||||
use shell_escape::unix::escape;
|
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use tokio::io::{AsyncWriteExt, BufReader};
|
|
||||||
|
|
||||||
use super::{CopyDirection, CopyOptions, Host};
|
use super::{CopyDirection, CopyOptions, Host, key_uploader};
|
||||||
use crate::nix::{StorePath, Profile, Goal, NixResult, NixCommand, NixError, Key, SYSTEM_PROFILE};
|
use crate::nix::{StorePath, Profile, Goal, NixResult, NixCommand, Key, SYSTEM_PROFILE};
|
||||||
use crate::util::{CommandExecution, capture_stream};
|
use crate::util::CommandExecution;
|
||||||
use crate::progress::TaskProgress;
|
use crate::progress::TaskProgress;
|
||||||
|
|
||||||
const DEPLOY_KEY_TEMPLATE: &'static str = include_str!("./deploy-key.template");
|
|
||||||
|
|
||||||
/// A remote machine connected over SSH.
|
/// A remote machine connected over SSH.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Ssh {
|
pub struct Ssh {
|
||||||
|
@ -203,13 +198,7 @@ impl Ssh {
|
||||||
self.progress_bar.log(&format!("Deploying key {}", name));
|
self.progress_bar.log(&format!("Deploying key {}", name));
|
||||||
|
|
||||||
let dest_path = key.dest_dir().join(name);
|
let dest_path = key.dest_dir().join(name);
|
||||||
|
let key_script = key_uploader::generate_script(key, &dest_path);
|
||||||
let key_script = DEPLOY_KEY_TEMPLATE.to_string()
|
|
||||||
.replace("%DESTINATION%", dest_path.to_str().unwrap())
|
|
||||||
.replace("%USER%", &escape(key.user().into()))
|
|
||||||
.replace("%GROUP%", &escape(key.group().into()))
|
|
||||||
.replace("%PERMISSIONS%", &escape(key.permissions().into()));
|
|
||||||
let key_script = escape(key_script.into());
|
|
||||||
|
|
||||||
let mut command = self.ssh(&["sh", "-c", &key_script]);
|
let mut command = self.ssh(&["sh", "-c", &key_script]);
|
||||||
|
|
||||||
|
@ -217,32 +206,7 @@ impl Ssh {
|
||||||
command.stderr(Stdio::piped());
|
command.stderr(Stdio::piped());
|
||||||
command.stdout(Stdio::piped());
|
command.stdout(Stdio::piped());
|
||||||
|
|
||||||
let mut reader = key.reader().await?;
|
let uploader = command.spawn()?;
|
||||||
|
key_uploader::feed_uploader(uploader, key, self.progress_bar.clone(), &mut self.logs).await
|
||||||
let mut child = command.spawn()?;
|
|
||||||
let mut stdin = child.stdin.take().unwrap();
|
|
||||||
tokio::io::copy(reader.as_mut(), &mut stdin).await?;
|
|
||||||
stdin.flush().await?;
|
|
||||||
drop(stdin);
|
|
||||||
|
|
||||||
let stdout = BufReader::new(child.stdout.take().unwrap());
|
|
||||||
let stderr = BufReader::new(child.stderr.take().unwrap());
|
|
||||||
|
|
||||||
let futures = join3(
|
|
||||||
capture_stream(stdout, self.progress_bar.clone()),
|
|
||||||
capture_stream(stderr, self.progress_bar.clone()),
|
|
||||||
child.wait(),
|
|
||||||
);
|
|
||||||
let (stdout_str, stderr_str, exit) = futures.await;
|
|
||||||
self.logs += &stdout_str;
|
|
||||||
self.logs += &stderr_str;
|
|
||||||
|
|
||||||
let exit = exit?;
|
|
||||||
|
|
||||||
if exit.success() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(NixError::NixFailure { exit_code: exit.code().unwrap() })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue