use std::fs;
use std::str::FromStr;
use std::time::Duration;
use anyhow::Result;
use bollard::Docker;
use bollard::errors::Error;
use bollard::image::{ImportImageOptions, TagImageOptions};
use bollard::models::BuildInfo;
use brane_dsl::DataType;
use brane_shr::formatters::PrettyListFormatter;
use brane_tsk::docker::{self, DockerOptions};
use chrono::{Local, Utc};
use console::{Alignment, pad_str, style};
use dialoguer::Confirm;
use fs_extra::dir;
use futures_util::stream::TryStreamExt;
use hyper::Body;
use indicatif::{DecimalBytes, HumanDuration};
use prettytable::Table;
use prettytable::format::FormatBuilder;
use specifications::container::Image;
use specifications::package::PackageInfo;
use specifications::version::Version;
use tokio::fs::File as TFile;
use tokio_stream::StreamExt;
use tokio_util::codec::{BytesCodec, FramedRead};
use crate::errors::PackageError;
use crate::utils::{ensure_package_dir, ensure_packages_dir};
fn insert_package_in_list(infos: &mut Vec<PackageInfo>, info: PackageInfo) {
for pkg in infos.iter_mut() {
debug!("Package '{}' vs '{}'", &info.name, &pkg.name);
if info.name.eq(&pkg.name) {
debug!(" > Version '{}' vs '{}'", info.version, pkg.version.to_string());
if info.version > pkg.version {
*pkg = info;
}
return;
}
}
infos.push(info);
}
pub fn inspect(name: String, version: Version, syntax: String) -> Result<()> {
let package_dir = ensure_package_dir(&name, Some(&version), false)?;
let package_file = package_dir.join("package.yml");
if let Ok(info) = PackageInfo::from_path(package_file) {
println!();
println!(
"Package {} ({} package, version {})",
style(&info.name).bold().cyan(),
style(format!("{}", info.kind)).bold(),
style(format!("{}", info.version)).bold()
);
println!(
"Created {} ({} ago)",
style(format!("{}", info.created.with_timezone(&Local))).bold().cyan(),
HumanDuration(Duration::from_secs((Local::now().time() - info.created.time()).num_seconds() as u64))
);
println!();
println!(
"Owners: {}",
if !info.owners.is_empty() {
format!("{}", PrettyListFormatter::new(info.owners.iter().map(|o| format!("{}", style(&o).bold())), "and"))
} else {
"<unspecified>".into()
}
);
println!("{}", if !info.description.trim().is_empty() { &info.description } else { "<no description>" });
println!();
println!("Classes provided by this package:");
let mut types: Vec<&String> = info.types.keys().collect();
types.sort_by_key(|t| t.to_lowercase());
for name in types {
let info = info.types.get(name).unwrap();
match syntax.as_str() {
"bscript" => {
println!(" - class {} {{", style(&name).bold().cyan());
for p in &info.properties {
println!(" {}: {};", style(&p.name).bold(), DataType::from(&p.data_type));
}
println!(" }}");
},
"bakery" => {
return Err(anyhow!("Bakery syntax is not yet implemented"));
},
"custom" => {
println!(" - Class {}", style(&name).bold().cyan());
for p in &info.properties {
println!(" {} {};", DataType::from(&p.data_type), style(&p.name).bold());
}
},
_ => {
return Err(anyhow!("Given syntax '{}' is unknown", syntax));
},
}
}
if info.types.is_empty() {
println!(" <none>");
}
println!();
println!("Functions provided by this package:");
let mut funcs: Vec<&String> = info.functions.keys().collect();
funcs.sort_by_key(|t| t.to_lowercase());
for name in funcs {
let func = info.functions.get(name).unwrap();
match syntax.as_str() {
"bscript" => {
println!(
" - func {}({}) -> {}",
style(&name).bold().cyan(),
func.parameters
.iter()
.map(|p| format!("{}: {}", style(&p.name).bold(), DataType::from(&p.data_type)))
.collect::<Vec<String>>()
.join(", "),
DataType::from(&func.return_type)
);
},
"bakery" => {
return Err(anyhow!("Bakery syntax is not yet implemented"));
},
"custom" => {
println!(" - Function {}", style(&name).bold().cyan());
println!(" - Arguments:");
for p in &func.parameters {
println!(" - {} {}", DataType::from(&p.data_type), style(&p.name).bold());
}
println!(" - Returns: {}", DataType::from(&func.return_type));
},
_ => {
return Err(anyhow!("Given syntax '{}' is unknown", syntax));
},
}
}
if info.functions.is_empty() {
println!(" <none>");
}
println!();
} else {
return Err(anyhow!("Failed to read package information."));
}
Ok(())
}
pub fn list(latest: bool) -> Result<(), PackageError> {
let packages_dir = match ensure_packages_dir(false) {
Ok(dir) => dir,
Err(_) => {
println!("No packages found.");
return Ok(());
},
};
let format = FormatBuilder::new().column_separator('\0').borders('\0').padding(1, 1).build();
let mut table = Table::new();
table.set_format(format);
table.add_row(row!["ID", "NAME", "VERSION", "KIND", "CREATED", "SIZE"]);
let index = match brane_tsk::local::get_package_index(&packages_dir) {
Ok(idx) => idx,
Err(err) => {
return Err(PackageError::IndexError { err });
},
};
let mut infos: Vec<PackageInfo> = Vec::with_capacity(index.packages.len());
for (_, info) in index.packages {
if latest {
insert_package_in_list(&mut infos, info);
} else {
infos.push(info);
}
}
let now = Utc::now().timestamp();
for entry in infos {
let package_path = packages_dir.join(&entry.name).join(entry.version.to_string());
let sversion = entry.version.to_string();
let uuid = format!("{}", &entry.id);
let id = pad_str(&uuid[..8], 10, Alignment::Left, Some(".."));
let name = pad_str(&entry.name, 20, Alignment::Left, Some(".."));
let version = pad_str(&sversion, 10, Alignment::Left, Some(".."));
let skind = format!("{}", entry.kind);
let kind = pad_str(&skind, 10, Alignment::Left, Some(".."));
let elapsed = Duration::from_secs((now - entry.created.timestamp()) as u64);
let created = format!("{} ago", HumanDuration(elapsed));
let created = pad_str(&created, 15, Alignment::Left, None);
let size = DecimalBytes(dir::get_size(package_path).unwrap());
table.add_row(row![id, name, version, kind, created, size]);
}
table.printstd();
Ok(())
}
pub async fn load(name: String, version: Version) -> Result<()> {
debug!("Loading package '{}' (version {})", name, &version);
let package_dir = ensure_package_dir(&name, Some(&version), false)?;
if !package_dir.exists() {
return Err(anyhow!("Package not found."));
}
let package_info = PackageInfo::from_path(package_dir.join("package.yml"))?;
let image = format!("{}:{}", package_info.name, package_info.version);
let image_file = package_dir.join("image.tar");
let docker = Docker::connect_with_local_defaults()?;
if docker.inspect_image(&image).await.is_ok() {
println!("Image already exists in local Docker deamon.");
return Ok(());
}
println!("Image doesn't exist in Docker deamon: importing...");
let options = ImportImageOptions { quiet: true };
let file_handle = TFile::open(&image_file).await;
if let Err(reason) = file_handle {
let code = reason.raw_os_error().unwrap_or(-1);
eprintln!("Could not open image file '{}': {}.", image_file.to_string_lossy(), reason);
std::process::exit(code);
}
let file = file_handle.ok().unwrap();
let byte_stream = FramedRead::new(file, BytesCodec::new()).map(|r| {
let bytes = r.unwrap().freeze();
Ok::<_, Error>(bytes)
});
let body = Body::wrap_stream(byte_stream);
let result = docker.import_image(options, body, None).try_collect::<Vec<_>>().await?;
if let Some(BuildInfo { stream: Some(stream), .. }) = result.first() {
debug!("{}", stream);
let (_, image_hash) = stream.trim().split_once("sha256:").unwrap_or_default();
if !image_hash.is_empty() {
debug!("Imported image: {}", image_hash);
let options = TagImageOptions { repo: &package_info.name, tag: &package_info.version.to_string() };
docker.tag_image(image_hash, Some(options)).await?;
}
}
Ok(())
}
pub async fn remove(force: bool, packages: Vec<(String, Version)>, docker_opts: DockerOptions) -> Result<(), PackageError> {
for (name, version) in packages {
if !version.is_latest() {
let package_dir = match ensure_package_dir(&name, Some(&version), false) {
Ok(dir) => dir,
Err(err) => {
return Err(PackageError::PackageVersionError { name, version, err });
},
};
if !force {
println!("Are you sure you want to remove package {} version {}?", style(&name).bold().cyan(), style(&version).bold().cyan());
println!();
let consent: bool = match Confirm::new().interact() {
Ok(consent) => consent,
Err(err) => {
return Err(PackageError::ConsentError { err });
},
};
if !consent {
return Ok(());
}
}
let package_info_path = package_dir.join("package.yml");
let package_info = match PackageInfo::from_path(package_info_path.clone()) {
Ok(info) => info,
Err(err) => {
return Err(PackageError::PackageInfoError { path: package_info_path, err });
},
};
let digest = match package_info.digest {
Some(digest) => digest,
None => {
return Err(PackageError::PackageInfoNoDigest { path: package_info_path });
},
};
let image: Image = Image::new(&package_info.name, Some(format!("{}", package_info.version)), Some(digest));
if let Err(err) = docker::remove_image(&docker_opts, &image).await {
return Err(PackageError::DockerRemoveError { image: Box::new(image), err });
}
if let Err(err) = fs::remove_dir_all(&package_dir) {
return Err(PackageError::PackageRemoveError { name, version, dir: package_dir, err });
}
let package_dir = match ensure_package_dir(&name, None, false) {
Ok(dir) => dir,
Err(err) => {
return Err(PackageError::PackageError { name, err });
},
};
match fs::read_dir(&package_dir) {
Ok(versions) => {
if versions.count() == 0 {
if let Err(err) = fs::remove_dir_all(&package_dir) {
return Err(PackageError::PackageRemoveError { name, version, dir: package_dir, err });
}
}
},
Err(err) => {
return Err(PackageError::VersionsError { name, dir: package_dir, err });
},
};
println!("Successfully removed version {} of package {}", style(&version).bold().cyan(), style(&name).bold().cyan());
return Ok(());
}
let package_dir = match ensure_package_dir(&name, None, false) {
Ok(dir) => dir,
Err(err) => {
return Err(PackageError::PackageError { name, err });
},
};
let versions: Vec<Version> = match fs::read_dir(&package_dir) {
Ok(versions) => {
let mut result = Vec::with_capacity(3);
for version in versions {
let version = match version {
Ok(version) => version,
Err(err) => {
return Err(PackageError::VersionsError { name, dir: package_dir, err });
},
};
let version = String::from(version.file_name().to_string_lossy());
let version = match Version::from_str(&version) {
Ok(version) => version,
Err(err) => {
return Err(PackageError::VersionParseError { name, raw: version, err });
},
};
result.push(version);
}
result
},
Err(err) => {
return Err(PackageError::VersionsError { name, dir: package_dir, err });
},
};
if !force {
println!("Are you sure you want to remove the following version(s) of package {}?", style(&name).bold().cyan());
for version in &versions {
println!("- {}", style(&version).bold().cyan());
}
println!();
let consent: bool = match Confirm::new().interact() {
Ok(consent) => consent,
Err(err) => {
return Err(PackageError::ConsentError { err });
},
};
if !consent {
continue;
}
}
for version in &versions {
let package_info_path = package_dir.join(version.to_string()).join("package.yml");
let package_info = match PackageInfo::from_path(package_info_path.clone()) {
Ok(info) => info,
Err(err) => {
return Err(PackageError::PackageInfoError { path: package_info_path, err });
},
};
let digest = match package_info.digest {
Some(digest) => digest,
None => {
return Err(PackageError::PackageInfoNoDigest { path: package_info_path });
},
};
let image: Image = Image::new(&package_info.name, Some(format!("{}", package_info.version)), Some(digest));
if let Err(err) = docker::remove_image(&docker_opts, &image).await {
return Err(PackageError::DockerRemoveError { image: Box::new(image), err });
}
}
if let Err(err) = fs::remove_dir_all(&package_dir) {
return Err(PackageError::PackageRemoveError { name, version, dir: package_dir, err });
}
println!("Successfully removed package {}", style(&name).bold().cyan());
}
Ok(())
}