use std::fmt;
use std::str::FromStr;
use isoloom_spec::model::{Shape, Target};
use super::family::Family;
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Configure {
Shell,
CloudInit,
Ansible,
}
impl Configure {
pub const ALL: [Configure; 3] = [Configure::Shell, Configure::CloudInit, Configure::Ansible];
#[must_use]
pub fn id(self) -> &'static str {
match self {
Configure::Shell => "shell",
Configure::CloudInit => "cloud-init",
Configure::Ansible => "ansible",
}
}
#[must_use]
pub fn default_on(target: Target, family: Family) -> Option<Configure> {
match target.needs() {
Shape::Docker | Shape::Cloud => None,
Shape::External => Some(Configure::Ansible),
Shape::Vm | Shape::VmOrLxc if target != Target::Vagrant && family != Family::Windows => Some(Configure::CloudInit),
Shape::Vm | Shape::VmOrLxc | Shape::Either => Some(Configure::Shell),
}
}
pub fn pairs_with(self, target: Target) -> Result<()> {
let why = match (target.needs(), self) {
(Shape::Docker, _) => "a container target bakes the recipe into its image: `--configure` doesn't apply",
(Shape::Cloud, _) => "cloud services have no machines to configure",
(_, Configure::CloudInit) if matches!(target, Target::Vagrant | Target::Hybrid) => {
"Vagrant's cloud-init is experimental and VirtualBox-only, and boxes don't ship cloud-init"
}
(Shape::External, Configure::CloudInit) => "existing hosts don't boot for cloud-init: it runs at a machine's first boot",
_ => return Ok(()),
};
Err(Error::Unpaired { tool: self, target, why })
}
}
impl fmt::Display for Configure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.id())
}
}
impl FromStr for Configure {
type Err = Error;
fn from_str(id: &str) -> Result<Configure> {
Configure::ALL
.into_iter()
.find(|c| c.id() == id)
.ok_or_else(|| Error::UnknownTool(id.to_owned()))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct OutPath(String);
impl OutPath {
#[must_use]
pub fn new(path: &str) -> Option<OutPath> {
let inside = !path.is_empty() && !path.starts_with('/') && !path.split('/').any(|p| p == ".." || p.is_empty());
inside.then(|| OutPath(path.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for OutPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Handoff {
Image {
dockerfile: OutPath,
},
Inline {
script: OutPath,
},
UserData {
document: OutPath,
},
Controller {
playbook: OutPath,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn each_target_defaults_to_todays_carrier() {
assert_eq!(Configure::default_on(Target::Docker, Family::Debian), None);
assert_eq!(Configure::default_on(Target::Vagrant, Family::Debian), Some(Configure::Shell));
assert_eq!(Configure::default_on(Target::Vagrant, Family::Windows), Some(Configure::Shell));
assert_eq!(Configure::default_on(Target::Proxmox, Family::RedHat), Some(Configure::CloudInit));
assert_eq!(Configure::default_on(Target::CloudVm, Family::Windows), Some(Configure::Shell));
assert_eq!(Configure::default_on(Target::External, Family::Debian), Some(Configure::Ansible));
assert_eq!(Configure::default_on(Target::Hybrid, Family::Debian), Some(Configure::Shell));
}
#[test]
fn a_tool_a_target_cant_hand_off_to_is_refused_with_why() {
let e = Configure::CloudInit.pairs_with(Target::Vagrant).unwrap_err();
assert!(
e.to_string()
.starts_with("vagrant: `--configure cloud-init`: Vagrant's cloud-init is experimental"),
"{e}"
);
assert!(Configure::Ansible.pairs_with(Target::Docker).is_err() && Configure::CloudInit.pairs_with(Target::External).is_err());
for target in [Target::Proxmox, Target::Esxi, Target::CloudVm] {
assert!(Configure::ALL.into_iter().all(|c| c.pairs_with(target).is_ok()), "{target:?}");
}
assert!(Configure::Ansible.pairs_with(Target::Vagrant).is_ok() && Configure::Shell.pairs_with(Target::External).is_ok());
}
#[test]
fn a_tool_reads_back_as_itself() {
for c in Configure::ALL {
assert_eq!(c.id().parse::<Configure>().ok(), Some(c));
}
assert!(
"puppet"
.parse::<Configure>()
.unwrap_err()
.to_string()
.contains("`shell`, `cloud-init` or `ansible`")
);
}
#[test]
fn a_generated_path_stays_in_the_project() {
assert_eq!(
OutPath::new(".isoloom/docker/build/web/Dockerfile").map(|p| p.to_string()).as_deref(),
Some(".isoloom/docker/build/web/Dockerfile")
);
for bad in ["", "/etc/x", "../x", "a//b"] {
assert_eq!(OutPath::new(bad), None, "{bad}");
}
}
}