mod compose;
mod terraform;
mod vagrant;
pub(crate) use compose::read_ps;
pub(crate) use terraform::plugin_cache;
use std::collections::BTreeMap;
use isoloom_manifest::{Action, Delivery, Identity, Manifest, SnakeName, Step, Template, Transport};
use tracing::Instrument;
use crate::Result;
use crate::error::Error;
use crate::executor::Cx;
use crate::status::{MachineStatus, ShellCommand};
use crate::vars::PortKey;
#[derive(Debug, Clone, Default)]
pub(crate) struct Observed {
pub(crate) machines: Vec<MachineStatus>,
pub(crate) published: BTreeMap<PortKey, u16>,
pub(crate) hosts: BTreeMap<String, String>,
pub(crate) outputs: BTreeMap<String, String>,
pub(crate) sensitive: std::collections::BTreeSet<String>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum Attacker<'a> {
Machine(&'a str),
AdHoc,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct Attached {
pub(crate) name: Option<String>,
pub(crate) network: Option<String>,
pub(crate) address: Option<String>,
}
pub(crate) trait Driver {
async fn preflight(&self, cx: &Cx<'_>) -> Result<()>;
fn prepare(&self, cx: &Cx<'_>) -> Result<()>;
fn env(&self, cx: &Cx<'_>) -> Result<Vec<(String, String)>>;
async fn action(&self, cx: &Cx<'_>, step: &Step, action: Action, with: &BTreeMap<SnakeName, Template>) -> Result<()>;
async fn observe(&self, cx: &Cx<'_>) -> Result<Observed>;
fn after_down(&self, cx: &Cx<'_>) -> Result<()>;
async fn attach(&self, cx: &Cx<'_>, attacker: Attacker<'_>, image: Option<&str>, network: Option<&str>) -> Result<Attached>;
async fn detach(&self, cx: &Cx<'_>, attacker: Attacker<'_>) -> Result<()>;
fn shell_command(&self, cx: &Cx<'_>, attacker: Attacker<'_>) -> Result<ShellCommand>;
}
#[derive(Debug, Clone)]
pub(crate) enum AnyDriver {
Compose(compose::Compose),
Vagrant(vagrant::Vagrant),
Terraform(terraform::Terraform),
}
impl AnyDriver {
pub(crate) fn for_manifest(manifest: &Manifest) -> Result<AnyDriver> {
let unsupported = |what: String| Err(Error::Unsupported { what });
let driver = match &manifest.identity {
Identity::Compose(identity) => AnyDriver::Compose(compose::Compose::new(identity.clone())),
Identity::Vagrant(identity) => AnyDriver::Vagrant(vagrant::Vagrant::new(identity.clone())),
Identity::Terraform(identity) => AnyDriver::Terraform(terraform::Terraform::new(identity.clone())),
};
let deliveries = manifest
.params
.iter()
.flat_map(|p| p.deliver.iter())
.chain(manifest.secrets.iter().flat_map(|s| s.deliver.iter()));
let terraform = matches!(driver, AnyDriver::Terraform(_));
for delivery in deliveries {
let ok = match delivery {
Delivery::Env { .. } | Delivery::StateFile { .. } => true,
Delivery::TfVar { .. } => terraform,
Delivery::Guest { transport, .. } => *transport == Transport::Ssh,
};
if !ok {
return unsupported(format!("a `{}` delivery", delivery_kind(delivery)));
}
}
Ok(driver)
}
fn name(&self) -> &'static str {
match self {
AnyDriver::Compose(_) => "compose",
AnyDriver::Vagrant(_) => "vagrant",
AnyDriver::Terraform(_) => "terraform",
}
}
pub(crate) async fn preflight(&self, cx: &Cx<'_>) -> Result<()> {
let span = tracing::debug_span!("driver", driver = self.name(), check = "preflight");
match self {
AnyDriver::Compose(d) => d.preflight(cx).instrument(span).await,
AnyDriver::Vagrant(d) => d.preflight(cx).instrument(span).await,
AnyDriver::Terraform(d) => d.preflight(cx).instrument(span).await,
}
}
pub(crate) fn elevates(&self, scope: &crate::scope::Scope) -> bool {
match self {
AnyDriver::Compose(_) | AnyDriver::Terraform(_) => false,
AnyDriver::Vagrant(d) => d.elevates(scope),
}
}
pub(crate) fn prepare(&self, cx: &Cx<'_>) -> Result<()> {
match self {
AnyDriver::Compose(d) => d.prepare(cx),
AnyDriver::Vagrant(d) => d.prepare(cx),
AnyDriver::Terraform(d) => d.prepare(cx),
}
}
pub(crate) fn env(&self, cx: &Cx<'_>) -> Result<Vec<(String, String)>> {
match self {
AnyDriver::Compose(d) => d.env(cx),
AnyDriver::Vagrant(d) => d.env(cx),
AnyDriver::Terraform(d) => d.env(cx),
}
}
pub(crate) async fn action(&self, cx: &Cx<'_>, step: &Step, action: Action, with: &BTreeMap<SnakeName, Template>) -> Result<()> {
let span = tracing::debug_span!("driver", driver = self.name(), action = ?action);
match self {
AnyDriver::Compose(d) => d.action(cx, step, action, with).instrument(span).await,
AnyDriver::Vagrant(d) => d.action(cx, step, action, with).instrument(span).await,
AnyDriver::Terraform(d) => d.action(cx, step, action, with).instrument(span).await,
}
}
pub(crate) async fn observe(&self, cx: &Cx<'_>) -> Result<Observed> {
let span = tracing::debug_span!("driver", driver = self.name(), read = "status");
match self {
AnyDriver::Compose(d) => d.observe(cx).instrument(span).await,
AnyDriver::Vagrant(d) => d.observe(cx).instrument(span).await,
AnyDriver::Terraform(d) => d.observe(cx).instrument(span).await,
}
}
pub(crate) fn after_down(&self, cx: &Cx<'_>) -> Result<()> {
match self {
AnyDriver::Compose(d) => d.after_down(cx),
AnyDriver::Vagrant(d) => d.after_down(cx),
AnyDriver::Terraform(d) => d.after_down(cx),
}
}
pub(crate) async fn attach(&self, cx: &Cx<'_>, attacker: Attacker<'_>, image: Option<&str>, network: Option<&str>) -> Result<Attached> {
let span = tracing::debug_span!("driver", driver = self.name(), op = "attach");
match self {
AnyDriver::Compose(d) => d.attach(cx, attacker, image, network).instrument(span).await,
AnyDriver::Vagrant(d) => d.attach(cx, attacker, image, network).instrument(span).await,
AnyDriver::Terraform(d) => d.attach(cx, attacker, image, network).instrument(span).await,
}
}
pub(crate) async fn detach(&self, cx: &Cx<'_>, attacker: Attacker<'_>) -> Result<()> {
let span = tracing::debug_span!("driver", driver = self.name(), op = "detach");
match self {
AnyDriver::Compose(d) => d.detach(cx, attacker).instrument(span).await,
AnyDriver::Vagrant(d) => d.detach(cx, attacker).instrument(span).await,
AnyDriver::Terraform(d) => d.detach(cx, attacker).instrument(span).await,
}
}
pub(crate) fn shell_command(&self, cx: &Cx<'_>, attacker: Attacker<'_>) -> Result<ShellCommand> {
match self {
AnyDriver::Compose(d) => d.shell_command(cx, attacker),
AnyDriver::Vagrant(d) => d.shell_command(cx, attacker),
AnyDriver::Terraform(d) => d.shell_command(cx, attacker),
}
}
}
pub(crate) fn foreign_action(driver: &str, step: &Step, action: Action) -> Error {
Error::Unsupported {
what: format!("step `{}`: the action `{}` on a {driver} run", step.id, action_name(action)),
}
}
pub(crate) async fn status_report(cx: &Cx<'_>) -> Result<String> {
crate::executor::capture_step(cx, "lifecycle.status", &cx.run.manifest().lifecycle.status).await
}
pub(crate) fn action_name(action: Action) -> String {
serde_json::to_value(action)
.ok()
.and_then(|v| v.as_str().map(str::to_owned))
.unwrap_or_default()
}
fn delivery_kind(d: &Delivery) -> &'static str {
match d {
Delivery::Env { .. } => "env",
Delivery::TfVar { .. } => "tf_var",
Delivery::StateFile { .. } => "state_file",
Delivery::Guest { .. } => "guest",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_what_this_runner_runs_is_accepted() {
assert!(matches!(AnyDriver::for_manifest(&crate::testing::docker_manifest()), Ok(AnyDriver::Compose(_))));
assert!(matches!(
AnyDriver::for_manifest(&crate::testing::vagrant_manifest()),
Ok(AnyDriver::Vagrant(_))
));
assert!(matches!(
AnyDriver::for_manifest(&crate::testing::proxmox_manifest()),
Ok(AnyDriver::Terraform(_))
));
let mut m = crate::testing::docker_manifest();
let tf: Delivery = serde_json::from_str(r#"{"via": "tf_var", "var": "inputs"}"#).unwrap();
m.secrets[0].deliver = isoloom_manifest::NonEmpty::one(tf);
assert_eq!(
AnyDriver::for_manifest(&m).unwrap_err().to_string(),
"a `tf_var` delivery isn't supported by this runner yet"
);
let mut cloud = crate::testing::proxmox_manifest();
let file: Delivery = serde_json::from_str(r#"{"via": "state_file", "path": "inputs.tfvars.json", "format": "tfvars_json"}"#).unwrap();
cloud.secrets[0].deliver = isoloom_manifest::NonEmpty::one(file);
assert!(AnyDriver::for_manifest(&cloud).is_ok(), "a state file");
let winrm: Delivery = serde_json::from_str(r#"{"via": "guest", "machine": "web", "transport": "winrm"}"#).unwrap();
cloud.secrets[0].deliver = isoloom_manifest::NonEmpty::one(winrm);
assert_eq!(
AnyDriver::for_manifest(&cloud).unwrap_err().to_string(),
"a `guest` delivery isn't supported by this runner yet",
"over SSH only"
);
}
}