use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use crate::entities::driver::PipelineDriver;
use crate::entities::environment::RunEnvironment;
use crate::entities::info::{ContentInfo, ShortName, info2str_opt, str2info_wl_opt};
use crate::entities::variables::Variable;
use crate::storage::use_from_storage;
#[derive(Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct PortBinding {
pub from: u16,
pub to: u16,
}
#[derive(Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Clone, Default)]
#[serde(rename_all = "snake_case")]
#[allow(missing_docs)]
pub enum ContainerExecutor {
Docker,
Podman,
#[default]
Auto,
}
impl ContainerExecutor {
pub fn is_docker(&self) -> bool {
match self {
Self::Docker => true,
Self::Podman => false,
Self::Auto => !matches!(
std::env::var("DEPLOYER_CONTAINER_DEFAULT_EXECUTOR").as_deref(),
Ok("podman")
),
}
}
pub fn is_default(&self) -> bool {
*self == Self::Auto
}
}
#[derive(Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Clone, Default)]
pub struct ContaineredOpts {
#[serde(skip_serializing_if = "Option::is_none")]
pub base_image: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub with: BTreeMap<String, ShortName>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub preflight_cmds: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub build_deployer_base_image: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub deployer_build_cmds: Vec<String>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub user: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cache_strategies: Vec<ContainerizedBuildStrategyStep>,
#[serde(default, skip_serializing_if = "crate::utils::is_false")]
pub run_detached: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub port_bindings: Vec<PortBinding>,
#[serde(default, skip_serializing_if = "crate::utils::is_false")]
pub allow_internal_host_bind: bool,
#[serde(default, skip_serializing_if = "crate::utils::is_false")]
pub use_containerd_local_storage_cache: bool,
#[serde(default, skip_serializing_if = "crate::utils::is_false")]
pub prevent_metadata_loading: bool,
#[serde(default, skip_serializing_if = "ContainerExecutor::is_default")]
pub executor: ContainerExecutor,
}
pub async fn try_enplace(
lines: &[String],
env: &RunEnvironment<'_>,
vars: &BTreeMap<String, Variable>,
) -> anyhow::Result<Vec<String>> {
let mut lines = lines.to_vec();
for line in &mut lines {
for (placeholder, variable) in vars.iter() {
if line.contains(placeholder) {
*line = line.replace(placeholder, &variable.get_value(env).await?);
}
}
}
Ok(lines)
}
pub fn copy_cmd(user: impl AsRef<str>, from: impl AsRef<str>, to: impl AsRef<str>) -> String {
format!(
"COPY {}{} {}",
if !user.as_ref().is_empty() {
format!("--chown={} ", user.as_ref())
} else {
"".to_string()
},
from.as_ref(),
to.as_ref()
)
}
impl ContaineredOpts {
pub fn sync_fake_content(&self, env: &RunEnvironment) -> anyhow::Result<()> {
for strategy in self.cache_strategies.iter() {
strategy.sync_fake_content(env)?;
}
Ok(())
}
pub async fn concat_strategies(
&self,
env: &RunEnvironment<'_>,
user: &str,
config_filepath: &str,
vars: &BTreeMap<String, Variable>,
) -> anyhow::Result<Option<String>> {
if !self.cache_strategies.is_empty() {
let mut strs = vec![];
for strategy in self.cache_strategies.iter() {
strs.push(strategy.concat(env, user, config_filepath, vars).await?);
}
Ok(Some(strs.join("\n")))
} else {
Ok(None)
}
}
}
#[derive(Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Clone, Default)]
pub struct ContainerizedBuildStrategyStep {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[serde(serialize_with = "info2str_opt", deserialize_with = "str2info_wl_opt")]
pub fake_content: Option<ContentInfo>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub copy_cmds: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub pre_cache_cmds: Vec<String>,
}
impl ContainerizedBuildStrategyStep {
fn sync_fake_content(&self, env: &RunEnvironment) -> anyhow::Result<()> {
if let Some(content_info) = &self.fake_content {
use_from_storage(env.storage_dir, env.run_dir, content_info)?;
}
Ok(())
}
async fn concat(
&self,
env: &RunEnvironment<'_>,
user: &str,
config_filepath: &str,
vars: &BTreeMap<String, Variable>,
) -> anyhow::Result<String> {
let copy_cmds = try_enplace(&self.copy_cmds, env, vars).await?;
let mut pre_cache_cmds = try_enplace(&self.pre_cache_cmds, env, vars).await?;
for cmd in &mut pre_cache_cmds {
if cmd.as_str().eq("DEPL") {
*cmd = match env.driver {
PipelineDriver::Deployer => {
format!(
"ENV DEPLOYER_CONTAINERED_BUILD=1\nRUN /depl --config deploy-config.yaml run {} --current\nENV DEPLOYER_CONTAINERED_BUILD=0",
env.master_pipeline,
)
}
PipelineDriver::Shell => {
format!("{}\nRUN /app/{config_filepath}", copy_cmd(user, config_filepath, "."))
}
};
}
}
let divider = match env.driver {
PipelineDriver::Deployer => format!("\n{}\n", copy_cmd(user, config_filepath, "deploy-config.yaml")),
PipelineDriver::Shell => "\n".to_string(),
};
Ok(copy_cmds.join("\n") + divider.as_str() + pre_cache_cmds.join("\n").as_str())
}
}
impl std::fmt::Display for ContainerizedBuildStrategyStep {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let parts = vec![
if let Some(fake) = self.fake_content.as_ref() {
format!("fake content: {}", fake.to_str())
} else {
String::new()
},
if !self.copy_cmds.is_empty() {
"some files to copy".to_string()
} else {
String::new()
},
if !self.pre_cache_cmds.is_empty() {
"some commands to cache".to_string()
} else {
String::new()
},
];
let mut strat_desc = parts
.into_iter()
.filter(|p| !p.is_empty())
.collect::<Vec<_>>()
.join(", ");
if strat_desc.is_empty() {
strat_desc = "empty step".to_string();
}
write!(f, "{strat_desc}")
}
}