use docker_compose::v2 as dc;
use docker_compose::v2::MergeOverride;
use std::collections::BTreeMap;
use std::collections::btree_map;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use ovr::Override;
use project::Project;
use util::Error;
#[derive(Debug)]
struct FileInfo {
rel_path: PathBuf,
file: dc::File,
}
impl FileInfo {
fn unnormalized(base_dir: &Path, rel_path: &Path) -> Result<FileInfo, Error> {
let path = base_dir.join(rel_path);
Ok(FileInfo {
rel_path: rel_path.to_owned(),
file: if path.exists() {
try!(dc::File::read_from_path(&path))
} else {
Default::default()
},
})
}
fn ensure_all_services_from(&mut self, base: &dc::File) {
for (name, _service) in base.services.iter() {
self.file.services.entry(name.to_owned())
.or_insert_with(Default::default);
}
}
fn finish_normalization(&mut self) {
let env_path = self.rel_path.parent().unwrap().join("common.env");
for (_name, service) in self.file.services.iter_mut() {
service.env_files.insert(0, dc::value(env_path.clone()));
}
}
}
#[derive(Debug)]
pub struct Pod {
base_dir: PathBuf,
name: String,
file_info: FileInfo,
override_file_infos: BTreeMap<Override, FileInfo>,
}
impl Pod {
#[doc(hidden)]
pub fn new<P, S>(base_dir: P, name: S, overrides: &[Override]) ->
Result<Pod, Error>
where P: Into<PathBuf>, S: Into<String>
{
let base_dir = base_dir.into();
let name = name.into();
let rel_path = Path::new(&format!("{}.yml", &name)).to_owned();
let mut file_info = try!(FileInfo::unnormalized(&base_dir, &rel_path));
file_info.finish_normalization();
let mut ovr_infos = BTreeMap::new();
for ovr in overrides {
let ovr_rel_path =
Path::new(&format!("overrides/{}/{}.yml", ovr.name(), &name))
.to_owned();
let mut ovr_info =
try!(FileInfo::unnormalized(&base_dir, &ovr_rel_path));
ovr_info.ensure_all_services_from(&file_info.file);
ovr_info.finish_normalization();
ovr_infos.insert(ovr.to_owned(), ovr_info);
}
Ok(Pod {
base_dir: base_dir,
name: name,
file_info: file_info,
override_file_infos: ovr_infos,
})
}
pub fn name(&self) -> &str {
&self.name
}
pub fn base_dir(&self) -> &Path {
&self.base_dir
}
pub fn rel_path(&self) -> &Path {
&self.file_info.rel_path
}
pub fn file(&self) -> &dc::File {
&self.file_info.file
}
fn override_file_info(&self, ovr: &Override) -> Result<&FileInfo, Error> {
self.override_file_infos.get(ovr).ok_or_else(|| {
err!("The override {} is not defined", ovr.name())
})
}
pub fn override_rel_path(&self, ovr: &Override) -> Result<&Path, Error> {
Ok(&(try!(self.override_file_info(ovr)).rel_path))
}
pub fn override_file(&self, ovr: &Override) -> Result<&dc::File, Error> {
Ok(&(try!(self.override_file_info(ovr)).file))
}
pub fn merged_file(&self, ovr: &Override) -> Result<dc::File, Error> {
debug!("Merging pod {} with override {}", self.name(), ovr.name());
Ok(self.file().merge_override(try!(self.override_file(ovr))))
}
pub fn override_files(&self) -> OverrideFiles {
OverrideFiles { iter: self.override_file_infos.iter() }
}
pub fn all_files(&self) -> AllFiles {
AllFiles {
pod: self,
state: AllFilesState::TopLevelFile,
}
}
pub fn compose_args(&self, proj: &Project, ovr: &Override) ->
Result<Vec<OsString>, Error>
{
let ovr_rel_path = try!(self.override_rel_path(ovr));
Ok(vec!("-p".into(), self.name().into(),
"-f".into(), proj.output_pods_dir().join(self.rel_path()).into(),
"-f".into(), proj.output_pods_dir().join(ovr_rel_path).into()))
}
}
pub struct OverrideFiles<'a> {
iter: btree_map::Iter<'a, Override, FileInfo>,
}
impl<'a> Iterator for OverrideFiles<'a> {
type Item = (&'a Override, &'a dc::File);
fn next(&mut self) -> Option<Self::Item> {
self.iter.next().map(|(ovr, file_info)| (ovr, &file_info.file))
}
}
enum AllFilesState<'a> {
TopLevelFile,
OverrideFiles(OverrideFiles<'a>),
}
pub struct AllFiles<'a> {
pod: &'a Pod,
state: AllFilesState<'a>,
}
impl<'a> Iterator for AllFiles<'a> {
type Item = &'a dc::File;
fn next(&mut self) -> Option<Self::Item> {
match self.state {
AllFilesState::TopLevelFile => {
self.state = AllFilesState::OverrideFiles(self.pod.override_files());
Some(self.pod.file())
}
AllFilesState::OverrideFiles(ref mut iter) => {
iter.next().map(|(_, file)| file)
}
}
}
}
#[test]
fn pods_are_normalized_on_load() {
use project::Project;
let proj = Project::from_example("hello").unwrap();
let frontend = proj.pod("frontend").unwrap();
let web = frontend.file().services.get("web").unwrap();
assert_eq!(web.env_files.len(), 1);
assert_eq!(web.env_files[0].value().unwrap(),
Path::new("common.env"));
let production = proj.ovr("production").unwrap();
let web_ovr = frontend.override_file(production).unwrap()
.services.get("web").unwrap();
assert_eq!(web_ovr.env_files.len(), 1);
assert_eq!(web_ovr.env_files[0].value().unwrap(),
Path::new("overrides/production/common.env"));
}
#[test]
fn can_merge_base_file_and_override() {
let proj: Project = Project::from_example("hello").unwrap();
let ovr = proj.ovr("development").unwrap();
let frontend = proj.pod("frontend").unwrap();
let merged = frontend.merged_file(&ovr).unwrap();
let proxy = merged.services.get("proxy").unwrap();
assert_eq!(proxy.env_files.len(), 2);
}