use std::{fmt::Write, sync::OnceLock};
use bson::{Document, doc};
use derive_builder::Builder;
use derive_default_builder::DefaultBuilder;
use partial_derive2::Partial;
use serde::{Deserialize, Serialize};
use strum::Display;
use typeshare::typeshare;
use crate::{
deserializers::{
env_vars_deserializer, item_or_vec_deserializer,
labels_deserializer, option_env_vars_deserializer,
option_item_or_vec_deserializer, option_labels_deserializer,
option_string_list_deserializer, string_list_deserializer,
},
entities::I64,
};
use super::{
SystemCommand, Version,
resource::{Resource, ResourceListItem, ResourceQuery},
};
#[cfg(feature = "utoipa")]
#[derive(utoipa::ToSchema)]
#[schema(as = Build)]
pub struct BuildSchema(
#[schema(inline)] pub Resource<BuildConfig, BuildInfo>,
);
#[typeshare]
pub type Build = Resource<BuildConfig, BuildInfo>;
impl Build {
pub fn get_image_names(&self) -> Vec<String> {
let Build {
name,
config:
BuildConfig {
image_name,
image_registry,
..
},
..
} = self;
let name = if image_name.is_empty() {
name
} else {
image_name
};
if image_registry.is_empty() {
return vec![name.to_string()];
}
image_registry
.iter()
.map(
|ImageRegistryConfig {
domain,
account,
organization,
}| {
match (
!domain.is_empty(),
!organization.is_empty(),
!account.is_empty(),
) {
(true, true, true) => {
format!("{domain}/{organization}/{name}")
}
(true, false, true) => {
format!("{domain}/{account}/{name}")
}
_ => name.to_string(),
}
},
)
.collect()
}
pub fn get_image_tags(
&self,
image_names: &[String],
commit_hash: Option<&str>,
additional: &[String],
) -> Vec<String> {
let BuildConfig {
version,
image_tag,
include_latest_tag,
include_version_tags: include_version_tag,
include_commit_tag,
..
} = &self.config;
let Version { major, minor, .. } = version;
let image_tag_postfix = if image_tag.is_empty() {
String::new()
} else {
format!("-{image_tag}")
};
let mut tags = Vec::new();
for image_name in image_names {
if !image_tag.is_empty() {
tags.push(format!("{image_name}:{image_tag}"));
}
if *include_latest_tag {
tags.push(format!("{image_name}:latest{image_tag_postfix}"));
}
if *include_version_tag {
tags
.push(format!("{image_name}:{version}{image_tag_postfix}"));
tags.push(format!(
"{image_name}:{major}.{minor}{image_tag_postfix}"
));
tags.push(format!("{image_name}:{major}{image_tag_postfix}"));
}
if *include_commit_tag && let Some(hash) = commit_hash {
tags.push(format!("{image_name}:{hash}{image_tag_postfix}"));
}
for tag in additional {
tags.push(format!("{image_name}:{tag}"))
}
}
tags
}
pub fn get_image_tags_as_arg(
&self,
commit_hash: Option<&str>,
additional: &[String],
) -> anyhow::Result<String> {
let mut res = String::new();
for image_tag in self.get_image_tags(
&self.get_image_names(),
commit_hash,
additional,
) {
write!(&mut res, " -t {image_tag}")?;
}
Ok(res)
}
}
#[typeshare]
pub type BuildListItem = ResourceListItem<BuildListItemInfo>;
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct BuildListItemInfo {
pub state: BuildState,
pub last_built_at: I64,
pub version: Version,
pub builder_id: String,
pub files_on_host: bool,
pub dockerfile_contents: bool,
pub linked_repo: String,
pub git_provider: String,
pub repo: String,
pub branch: String,
pub repo_link: String,
pub built_hash: Option<String>,
pub latest_hash: Option<String>,
pub image_registry_domain: Option<String>,
}
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
Default,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
Display,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub enum BuildState {
Building,
Ok,
Failed,
#[default]
Unknown,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct BuildInfo {
pub last_built_at: I64,
pub built_hash: Option<String>,
pub built_message: Option<String>,
pub built_contents: Option<String>,
pub remote_path: Option<String>,
pub remote_contents: Option<String>,
pub remote_error: Option<String>,
pub latest_hash: Option<String>,
pub latest_message: Option<String>,
}
#[typeshare(serialized_as = "Partial<BuildConfig>")]
pub type _PartialBuildConfig = PartialBuildConfig;
#[typeshare]
#[derive(Debug, Clone, Serialize, Deserialize, Builder, Partial)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[partial_derive(Debug, Clone, Default, Serialize, Deserialize)]
#[diff_derive(Debug, Clone, Default, Serialize, Deserialize)]
#[partial(skip_serializing_none, from, diff)]
pub struct BuildConfig {
#[serde(default, alias = "builder")]
#[partial_attr(serde(alias = "builder"))]
#[builder(default)]
pub builder_id: String,
#[serde(default)]
#[builder(default)]
pub version: Version,
#[serde(default = "default_auto_increment_version")]
#[builder(default = "default_auto_increment_version()")]
#[partial_default(default_auto_increment_version())]
pub auto_increment_version: bool,
#[serde(default)]
#[builder(default)]
pub image_name: String,
#[serde(default)]
#[builder(default)]
pub image_tag: String,
#[serde(default = "default_include_tag")]
#[builder(default = "default_include_tag()")]
#[partial_default(default_include_tag())]
pub include_latest_tag: bool,
#[serde(default = "default_include_tag")]
#[builder(default = "default_include_tag()")]
#[partial_default(default_include_tag())]
pub include_version_tags: bool,
#[serde(default = "default_include_tag")]
#[builder(default = "default_include_tag()")]
#[partial_default(default_include_tag())]
pub include_commit_tag: bool,
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub links: Vec<String>,
#[serde(default)]
#[builder(default)]
pub linked_repo: String,
#[serde(default = "default_git_provider")]
#[builder(default = "default_git_provider()")]
#[partial_default(default_git_provider())]
pub git_provider: String,
#[serde(default = "default_git_https")]
#[builder(default = "default_git_https()")]
#[partial_default(default_git_https())]
pub git_https: bool,
#[serde(default)]
#[builder(default)]
pub git_account: String,
#[serde(default)]
#[builder(default)]
pub repo: String,
#[serde(default = "default_branch")]
#[builder(default = "default_branch()")]
#[partial_default(default_branch())]
pub branch: String,
#[serde(default)]
#[builder(default)]
pub commit: String,
#[serde(default = "default_webhook_enabled")]
#[builder(default = "default_webhook_enabled()")]
#[partial_default(default_webhook_enabled())]
pub webhook_enabled: bool,
#[serde(default)]
#[builder(default)]
pub webhook_secret: String,
#[serde(default)]
#[builder(default)]
pub files_on_host: bool,
#[serde(default = "default_build_path")]
#[builder(default = "default_build_path()")]
#[partial_default(default_build_path())]
pub build_path: String,
#[serde(default = "default_dockerfile_path")]
#[builder(default = "default_dockerfile_path()")]
#[partial_default(default_dockerfile_path())]
pub dockerfile_path: String,
#[serde(default, deserialize_with = "item_or_vec_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_item_or_vec_deserializer"
))]
#[builder(default)]
pub image_registry: Vec<ImageRegistryConfig>,
#[serde(default)]
#[builder(default)]
pub skip_secret_interp: bool,
#[serde(default)]
#[builder(default)]
pub use_buildx: bool,
#[serde(default, deserialize_with = "string_list_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_string_list_deserializer"
))]
#[builder(default)]
pub extra_args: Vec<String>,
#[serde(default)]
#[builder(default)]
pub pre_build: SystemCommand,
#[serde(default)]
#[builder(default)]
pub dockerfile: String,
#[serde(default, deserialize_with = "env_vars_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_env_vars_deserializer"
))]
#[builder(default)]
pub build_args: String,
#[serde(default, deserialize_with = "env_vars_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_env_vars_deserializer"
))]
#[builder(default)]
pub secret_args: String,
#[serde(default, deserialize_with = "labels_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_labels_deserializer"
))]
#[builder(default)]
pub labels: String,
}
impl BuildConfig {
pub fn builder() -> BuildConfigBuilder {
BuildConfigBuilder::default()
}
}
fn default_auto_increment_version() -> bool {
true
}
fn default_include_tag() -> bool {
true
}
fn default_git_provider() -> String {
String::from("github.com")
}
fn default_git_https() -> bool {
true
}
fn default_branch() -> String {
String::from("main")
}
fn default_build_path() -> String {
String::from(".")
}
fn default_dockerfile_path() -> String {
String::from("Dockerfile")
}
fn default_webhook_enabled() -> bool {
true
}
impl Default for BuildConfig {
fn default() -> Self {
Self {
builder_id: Default::default(),
skip_secret_interp: Default::default(),
version: Default::default(),
auto_increment_version: default_auto_increment_version(),
image_name: Default::default(),
image_tag: Default::default(),
include_latest_tag: default_include_tag(),
include_version_tags: default_include_tag(),
include_commit_tag: default_include_tag(),
links: Default::default(),
linked_repo: Default::default(),
git_provider: default_git_provider(),
git_https: default_git_https(),
repo: Default::default(),
branch: default_branch(),
commit: Default::default(),
git_account: Default::default(),
pre_build: Default::default(),
build_path: default_build_path(),
dockerfile_path: default_dockerfile_path(),
build_args: Default::default(),
secret_args: Default::default(),
labels: Default::default(),
extra_args: Default::default(),
use_buildx: Default::default(),
image_registry: Default::default(),
webhook_enabled: default_webhook_enabled(),
webhook_secret: Default::default(),
dockerfile: Default::default(),
files_on_host: Default::default(),
}
}
}
#[cfg(feature = "utoipa")]
impl utoipa::PartialSchema for PartialBuildConfig {
fn schema()
-> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
utoipa::schema!(#[inline] std::collections::HashMap<String, serde_json::Value>).into()
}
}
#[cfg(feature = "utoipa")]
impl utoipa::ToSchema for PartialBuildConfig {}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct ImageRegistryConfig {
#[serde(default)]
pub domain: String,
#[serde(default)]
pub account: String,
#[serde(default)]
pub organization: String,
}
impl ImageRegistryConfig {
pub fn static_default() -> &'static ImageRegistryConfig {
static DEFAULT: OnceLock<ImageRegistryConfig> = OnceLock::new();
DEFAULT.get_or_init(Default::default)
}
}
#[typeshare]
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct BuildActionState {
pub building: bool,
}
#[typeshare]
pub type BuildQuery = ResourceQuery<BuildQuerySpecifics>;
#[typeshare]
#[derive(
Debug, Clone, Default, Serialize, Deserialize, DefaultBuilder,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct BuildQuerySpecifics {
#[serde(default)]
pub builder_ids: Vec<String>,
#[serde(default)]
pub repos: Vec<String>,
#[serde(default)]
pub built_since: I64,
}
impl super::resource::AddFilters for BuildQuerySpecifics {
fn add_filters(&self, filters: &mut Document) {
if !self.builder_ids.is_empty() {
filters.insert(
"config.builder_id",
doc! { "$in": &self.builder_ids },
);
}
if !self.repos.is_empty() {
filters.insert("config.repo", doc! { "$in": &self.repos });
}
if self.built_since > 0 {
filters.insert(
"info.last_built_at",
doc! { "$gte": self.built_since },
);
}
}
}