use anyhow::Context;
use bson::{Document, doc};
use derive_builder::Builder;
use derive_default_builder::DefaultBuilder;
use partial_derive2::Partial;
use serde::{Deserialize, Serialize};
use strum::{AsRefStr, Display, EnumDiscriminants, EnumString};
use typeshare::typeshare;
use crate::{
deserializers::{
conversions_deserializer, env_vars_deserializer,
labels_deserializer, option_conversions_deserializer,
option_env_vars_deserializer, option_labels_deserializer,
option_string_list_deserializer, option_term_labels_deserializer,
string_list_deserializer, term_labels_deserializer,
},
entities::{
EnvironmentVar, ImageDigest, environment_vars_from_str,
optional_str,
},
parsers::parse_key_value_list,
};
use super::{
TerminationSignal, Version,
docker::container::ContainerStateStatusEnum,
resource::{Resource, ResourceListItem, ResourceQuery},
};
#[cfg(feature = "utoipa")]
#[derive(utoipa::ToSchema)]
#[schema(as = Deployment)]
pub struct DeploymentSchema(
#[schema(inline)]
pub Resource<DeploymentConfig, crate::entities::NoData>,
);
#[typeshare]
pub type Deployment = Resource<DeploymentConfig, DeploymentInfo>;
#[typeshare]
pub type DeploymentListItem =
ResourceListItem<DeploymentListItemInfo>;
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct DeploymentListItemInfo {
pub state: DeploymentState,
pub status: Option<String>,
pub image: String,
pub update_available: bool,
pub swarm_id: String,
pub server_id: String,
pub build_id: Option<String>,
}
#[typeshare]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct DeploymentInfo {
#[serde(default)]
pub latest_image_digest: ImageDigest,
}
#[typeshare(serialized_as = "Partial<DeploymentConfig>")]
pub type _PartialDeploymentConfig = PartialDeploymentConfig;
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Builder, Partial)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[partial_derive(Serialize, Deserialize, Debug, Clone, Default)]
#[diff_derive(Serialize, Deserialize, Debug, Clone, Default)]
#[partial(skip_serializing_none, from, diff)]
pub struct DeploymentConfig {
#[serde(default, alias = "swarm")]
#[partial_attr(serde(alias = "swarm"))]
#[builder(default)]
pub swarm_id: String,
#[serde(default, alias = "server")]
#[partial_attr(serde(alias = "server"))]
#[builder(default)]
pub server_id: String,
#[serde(default)]
#[builder(default)]
pub image: DeploymentImage,
#[serde(default)]
#[builder(default)]
pub image_registry_account: String,
#[serde(default)]
#[builder(default)]
pub skip_secret_interp: bool,
#[serde(default)]
#[builder(default)]
pub redeploy_on_build: bool,
#[serde(default)]
#[builder(default)]
pub poll_for_updates: bool,
#[serde(default)]
#[builder(default)]
pub auto_update: bool,
#[serde(default = "default_send_alerts")]
#[builder(default = "default_send_alerts()")]
#[partial_default(default_send_alerts())]
pub send_alerts: bool,
#[serde(default)]
#[builder(default)]
pub links: Vec<String>,
#[serde(default = "default_network")]
#[builder(default = "default_network()")]
#[partial_default(default_network())]
pub network: String,
#[serde(default)]
#[builder(default)]
pub restart: RestartMode,
#[serde(default)]
#[builder(default)]
pub command: String,
#[serde(default = "default_replicas")]
#[builder(default = "default_replicas()")]
#[partial_default(default_replicas())]
pub replicas: i32,
#[serde(default)]
#[builder(default)]
pub termination_signal: TerminationSignal,
#[serde(default = "default_termination_timeout")]
#[builder(default = "default_termination_timeout()")]
#[partial_default(default_termination_timeout())]
pub termination_timeout: i32,
#[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, deserialize_with = "term_labels_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_term_labels_deserializer"
))]
#[builder(default)]
pub term_signal_labels: String,
#[serde(default, deserialize_with = "conversions_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_conversions_deserializer"
))]
#[builder(default)]
pub ports: String,
#[serde(default, deserialize_with = "conversions_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_conversions_deserializer"
))]
#[builder(default)]
pub volumes: String,
#[serde(default, deserialize_with = "env_vars_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_env_vars_deserializer"
))]
#[builder(default)]
pub environment: String,
#[serde(default, deserialize_with = "labels_deserializer")]
#[partial_attr(serde(
default,
deserialize_with = "option_labels_deserializer"
))]
#[builder(default)]
pub labels: String,
}
impl DeploymentConfig {
pub fn builder() -> DeploymentConfigBuilder {
DeploymentConfigBuilder::default()
}
pub fn env_vars(&self) -> anyhow::Result<Vec<EnvironmentVar>> {
environment_vars_from_str(&self.environment)
.context("Invalid environment")
}
}
fn default_replicas() -> i32 {
1
}
fn default_send_alerts() -> bool {
true
}
fn default_termination_timeout() -> i32 {
10
}
fn default_network() -> String {
String::from("host")
}
impl Default for DeploymentConfig {
fn default() -> Self {
Self {
swarm_id: Default::default(),
server_id: Default::default(),
image: Default::default(),
image_registry_account: Default::default(),
skip_secret_interp: Default::default(),
redeploy_on_build: Default::default(),
poll_for_updates: Default::default(),
auto_update: Default::default(),
send_alerts: default_send_alerts(),
links: Default::default(),
network: default_network(),
restart: Default::default(),
command: Default::default(),
replicas: default_replicas(),
termination_signal: Default::default(),
termination_timeout: default_termination_timeout(),
extra_args: Default::default(),
term_signal_labels: Default::default(),
ports: Default::default(),
volumes: Default::default(),
environment: Default::default(),
labels: Default::default(),
}
}
}
#[cfg(feature = "utoipa")]
impl utoipa::PartialSchema for PartialDeploymentConfig {
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 PartialDeploymentConfig {}
#[typeshare]
#[derive(
Serialize, Deserialize, Debug, Clone, PartialEq, EnumDiscriminants,
)]
#[strum_discriminants(name(DeploymentImageVariant))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(
not(feature = "utoipa"),
strum_discriminants(derive(
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
Display,
EnumString,
AsRefStr
))
)]
#[cfg_attr(
feature = "utoipa",
strum_discriminants(derive(
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
Display,
EnumString,
AsRefStr,
utoipa::ToSchema
))
)]
#[serde(tag = "type", content = "params")]
pub enum DeploymentImage {
Image {
#[serde(default)]
image: String,
},
Build {
#[serde(default, alias = "build")]
build_id: String,
#[serde(default)]
version: Version,
},
}
impl Default for DeploymentImage {
fn default() -> Self {
Self::Image {
image: Default::default(),
}
}
}
impl DeploymentImage {
pub fn as_image(&self) -> Option<&str> {
let Self::Image { image } = self else {
return None;
};
optional_str(image)
}
}
#[typeshare]
#[derive(
Debug, Clone, Default, PartialEq, Serialize, Deserialize,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct Conversion {
pub local: String,
pub container: String,
}
pub fn conversions_from_str(
input: &str,
) -> anyhow::Result<Vec<Conversion>> {
parse_key_value_list(input).map(|conversions| {
conversions
.into_iter()
.map(|(local, container)| Conversion { local, container })
.collect()
})
}
#[typeshare]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
Default,
Display,
EnumString,
Serialize,
Deserialize,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum DeploymentState {
Deploying,
Running,
Created,
Restarting,
Removing,
Paused,
Exited,
Dead,
Unhealthy,
NotDeployed,
#[default]
Unknown,
}
impl From<ContainerStateStatusEnum> for DeploymentState {
fn from(value: ContainerStateStatusEnum) -> Self {
match value {
ContainerStateStatusEnum::Empty => DeploymentState::Unknown,
ContainerStateStatusEnum::Created => DeploymentState::Created,
ContainerStateStatusEnum::Running => DeploymentState::Running,
ContainerStateStatusEnum::Paused => DeploymentState::Paused,
ContainerStateStatusEnum::Restarting => {
DeploymentState::Restarting
}
ContainerStateStatusEnum::Removing => DeploymentState::Removing,
ContainerStateStatusEnum::Exited => DeploymentState::Exited,
ContainerStateStatusEnum::Dead => DeploymentState::Dead,
}
}
}
#[typeshare]
#[derive(
Serialize,
Deserialize,
Debug,
PartialEq,
Hash,
Eq,
Clone,
Copy,
Default,
Display,
EnumString,
AsRefStr,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub enum RestartMode {
#[default]
#[serde(rename = "no")]
#[strum(serialize = "no")]
NoRestart,
#[serde(rename = "on-failure")]
#[strum(serialize = "on-failure")]
OnFailure,
#[serde(rename = "always")]
#[strum(serialize = "always")]
Always,
#[serde(rename = "unless-stopped")]
#[strum(serialize = "unless-stopped")]
UnlessStopped,
}
#[typeshare]
#[derive(
Serialize,
Deserialize,
Debug,
Clone,
Default,
PartialEq,
Eq,
Builder,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct TerminationSignalLabel {
#[builder(default)]
pub signal: TerminationSignal,
#[builder(default)]
pub label: String,
}
pub fn term_signal_labels_from_str(
input: &str,
) -> anyhow::Result<Vec<TerminationSignalLabel>> {
parse_key_value_list(input).and_then(|list| {
list
.into_iter()
.map(|(signal, label)| {
anyhow::Ok(TerminationSignalLabel {
signal: signal.parse()?,
label,
})
})
.collect()
})
}
#[typeshare]
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct DeploymentActionState {
pub pulling: bool,
pub deploying: bool,
pub updating: bool,
pub starting: bool,
pub restarting: bool,
pub pausing: bool,
pub unpausing: bool,
pub stopping: bool,
pub destroying: bool,
pub renaming: bool,
}
#[typeshare]
pub type DeploymentQuery = ResourceQuery<DeploymentQuerySpecifics>;
#[typeshare]
#[derive(
Debug, Clone, Default, Serialize, Deserialize, DefaultBuilder,
)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct DeploymentQuerySpecifics {
#[serde(default)]
pub server_ids: Vec<String>,
#[serde(default)]
pub build_ids: Vec<String>,
#[serde(default)]
pub update_available: bool,
}
impl super::resource::AddFilters for DeploymentQuerySpecifics {
fn add_filters(&self, filters: &mut Document) {
if !self.server_ids.is_empty() {
filters
.insert("config.server_id", doc! { "$in": &self.server_ids });
}
if !self.build_ids.is_empty() {
filters.insert("config.image.type", "Build");
filters.insert(
"config.image.params.build_id",
doc! { "$in": &self.build_ids },
);
}
}
}
pub fn extract_registry_domain(
image_name: &str,
) -> anyhow::Result<String> {
let mut split = image_name.split('/');
let maybe_domain =
split.next().context("image name cannot be empty string")?;
if maybe_domain.contains('.') {
Ok(maybe_domain.to_string())
} else {
Ok(String::from("docker.io"))
}
}