diff --git a/client/core/rs/src/entities/build.rs b/client/core/rs/src/entities/build.rs index 60850279d..01b58ec5c 100644 --- a/client/core/rs/src/entities/build.rs +++ b/client/core/rs/src/entities/build.rs @@ -122,12 +122,24 @@ pub struct BuildConfig { pub dockerfile_path: String, /// Docker build arguments - #[serde(default)] + #[serde( + default, + deserialize_with = "super::env_vars_deserializer" + )] + #[partial_attr(serde( + deserialize_with = "super::option_env_vars_deserializer" + ))] #[builder(default)] pub build_args: Vec, /// Docker labels - #[serde(default)] + #[serde( + default, + deserialize_with = "super::env_vars_deserializer" + )] + #[partial_attr(serde( + deserialize_with = "super::option_env_vars_deserializer" + ))] #[builder(default)] pub labels: Vec, diff --git a/client/core/rs/src/entities/deployment.rs b/client/core/rs/src/entities/deployment.rs index f1119748a..11ed9bb21 100644 --- a/client/core/rs/src/entities/deployment.rs +++ b/client/core/rs/src/entities/deployment.rs @@ -1,11 +1,15 @@ use std::collections::HashMap; +use anyhow::Context; use bson::{doc, Document}; use derive_builder::Builder; use derive_default_builder::DefaultBuilder; use derive_variants::EnumVariants; use partial_derive2::Partial; -use serde::{Deserialize, Serialize}; +use serde::{ + de::{value::SeqAccessDeserializer, Visitor}, + Deserialize, Deserializer, Serialize, +}; use strum::{Display, EnumString}; use typeshare::typeshare; @@ -86,7 +90,13 @@ pub struct DeploymentConfig { /// Labels attached to various termination signal options. /// Used to specify different shutdown functionality depending on the termination signal. - #[serde(default = "default_term_signal_labels")] + #[serde( + default = "default_term_signal_labels", + deserialize_with = "term_labels_deserializer" + )] + #[partial_attr(serde( + deserialize_with = "option_term_labels_deserializer" + ))] #[builder(default = "default_term_signal_labels()")] #[partial_default(default_term_signal_labels())] pub term_signal_labels: Vec, @@ -105,23 +115,41 @@ pub struct DeploymentConfig { /// The container port mapping. /// Irrelevant if container network is `host`. /// Maps ports on host to ports on container. - #[serde(default)] + #[serde(default, deserialize_with = "conversions_deserializer")] + #[partial_attr(serde( + deserialize_with = "option_conversions_deserializer" + ))] #[builder(default)] pub ports: Vec, /// The container volume mapping. /// Maps files / folders on host to files / folders in container. - #[serde(default)] + #[serde(default, deserialize_with = "conversions_deserializer")] + #[partial_attr(serde( + deserialize_with = "option_conversions_deserializer" + ))] #[builder(default)] pub volumes: Vec, /// The environment variables passed to the container. - #[serde(default)] + #[serde( + default, + deserialize_with = "super::env_vars_deserializer" + )] + #[partial_attr(serde( + deserialize_with = "super::option_env_vars_deserializer" + ))] #[builder(default)] pub environment: Vec, /// The docker labels given to the container. - #[serde(default)] + #[serde( + default, + deserialize_with = "super::env_vars_deserializer" + )] + #[partial_attr(serde( + deserialize_with = "super::option_env_vars_deserializer" + ))] #[builder(default)] pub labels: Vec, @@ -244,7 +272,7 @@ impl Default for DeploymentImage { #[typeshare] #[derive( - Serialize, Deserialize, Debug, Clone, Default, PartialEq, + Debug, Clone, Default, PartialEq, Serialize, Deserialize, )] pub struct Conversion { /// reference on the server. @@ -253,6 +281,144 @@ pub struct Conversion { pub container: String, } +pub fn conversions_from_str( + value: &str, +) -> anyhow::Result> { + let res = value + .split('\n') + .map(|line| line.trim()) + .enumerate() + .filter(|(_, line)| !line.starts_with('#')) + .map(|(i, line)| { + let mut split = line.split('='); + let local = split + .next() + .with_context(|| { + format!("line {i} does not have 'local' key") + })? + .trim() + .to_string(); + // remove trailing comments + let mut container_split = split + .next() + .with_context(|| { + format!("line {i} does not have 'container' key") + })? + .split('#'); + let container = container_split + .next() + .with_context(|| { + format!("line {i} does not have 'container' key") + })? + .trim() + .to_string(); + anyhow::Ok(Conversion { local, container }) + }) + .collect::>>()?; + Ok(res) +} + +pub fn conversions_deserializer<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(ConversionVisitor) +} + +pub fn option_conversions_deserializer<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(OptionConversionVisitor) +} + +struct ConversionVisitor; + +impl<'de> Visitor<'de> for ConversionVisitor { + type Value = Vec; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter, + ) -> std::fmt::Result { + write!(formatter, "string or Vec") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + conversions_from_str(v) + .map_err(|e| serde::de::Error::custom(format!("{e:#}"))) + } + + fn visit_seq(self, seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + #[derive(Deserialize)] + struct ConversionInner { + local: String, + container: String, + } + + impl From for Conversion { + fn from(value: ConversionInner) -> Self { + Self { + local: value.local, + container: value.container, + } + } + } + + let res = Vec::::deserialize( + SeqAccessDeserializer::new(seq), + )? + .into_iter() + .map(Into::into) + .collect(); + Ok(res) + } +} + +struct OptionConversionVisitor; + +impl<'de> Visitor<'de> for OptionConversionVisitor { + type Value = Option>; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter, + ) -> std::fmt::Result { + write!(formatter, "null or string or Vec") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + ConversionVisitor.visit_str(v).map(Some) + } + + fn visit_seq(self, seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + ConversionVisitor.visit_seq(seq).map(Some) + } + + fn visit_none(self) -> Result + where + E: serde::de::Error, + { + Ok(None) + } +} + /// A summary of a docker container on a server. #[typeshare] #[derive(Serialize, Deserialize, Debug, Clone)] @@ -401,6 +567,141 @@ pub struct TerminationSignalLabel { pub label: String, } +pub fn term_signal_labels_from_str( + value: &str, +) -> anyhow::Result> { + let res = value + .split('\n') + .map(|line| line.trim()) + .enumerate() + .filter(|(_, line)| !line.starts_with('#')) + .map(|(i, line)| { + let mut split = line.split('='); + let signal = split + .next() + .with_context(|| format!("line {i} does not have signal"))? + .trim() + .parse::() + .with_context(|| { + format!("line {i} does not have valid signal") + })?; + // remove trailing comments + let mut label_split = split + .next() + .with_context(|| format!("line {i} does not have label"))? + .split('#'); + let label = label_split + .next() + .with_context(|| format!("line {i} does not have label"))? + .trim() + .to_string(); + anyhow::Ok(TerminationSignalLabel { signal, label }) + }) + .collect::>>()?; + Ok(res) +} + +pub fn term_labels_deserializer<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(TermSignalLabelVisitor) +} + +pub fn option_term_labels_deserializer<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(OptionTermSignalLabelVisitor) +} + +struct TermSignalLabelVisitor; + +impl<'de> Visitor<'de> for TermSignalLabelVisitor { + type Value = Vec; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter, + ) -> std::fmt::Result { + write!(formatter, "string or Vec") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + term_signal_labels_from_str(v) + .map_err(|e| serde::de::Error::custom(format!("{e:#}"))) + } + + fn visit_seq(self, seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + #[derive(Deserialize)] + struct TermSignalLabelInner { + signal: TerminationSignal, + label: String, + } + + impl From for TerminationSignalLabel { + fn from(value: TermSignalLabelInner) -> Self { + Self { + signal: value.signal, + label: value.label, + } + } + } + + let res = Vec::::deserialize( + SeqAccessDeserializer::new(seq), + )? + .into_iter() + .map(Into::into) + .collect(); + Ok(res) + } +} + +struct OptionTermSignalLabelVisitor; + +impl<'de> Visitor<'de> for OptionTermSignalLabelVisitor { + type Value = Option>; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter, + ) -> std::fmt::Result { + write!(formatter, "null or string or Vec") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + TermSignalLabelVisitor.visit_str(v).map(Some) + } + + fn visit_seq(self, seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + TermSignalLabelVisitor.visit_seq(seq).map(Some) + } + + fn visit_none(self) -> Result + where + E: serde::de::Error, + { + Ok(None) + } +} + #[typeshare] #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] pub struct DeploymentActionState { diff --git a/client/core/rs/src/entities/mod.rs b/client/core/rs/src/entities/mod.rs index 192265b22..b15fbcd3c 100644 --- a/client/core/rs/src/entities/mod.rs +++ b/client/core/rs/src/entities/mod.rs @@ -6,8 +6,11 @@ use build::CloudRegistryConfig; use clap::Parser; use derive_empty_traits::EmptyTraits; use serde::{ - de::{value::MapAccessDeserializer, Visitor}, - Deserialize, Serialize, + de::{ + value::{MapAccessDeserializer, SeqAccessDeserializer}, + Visitor, + }, + Deserialize, Deserializer, Serialize, }; use serror::Serror; use strum::{AsRefStr, Display, EnumString}; @@ -324,13 +327,145 @@ impl Version { #[typeshare] #[derive( - Serialize, Deserialize, Debug, Clone, Default, PartialEq, + Debug, Clone, Default, PartialEq, Serialize, Deserialize, )] pub struct EnvironmentVar { pub variable: String, pub value: String, } +pub fn environment_vars_from_str( + value: &str, +) -> anyhow::Result> { + let res = value + .split('\n') + .map(|line| line.trim()) + .enumerate() + .filter(|(_, line)| !line.starts_with('#')) + .map(|(i, line)| { + let mut split = line.split('='); + let variable = split + .next() + .with_context(|| format!("line {i} does not have variable"))? + .trim() + .to_string(); + // remove trailing comments + let mut value_split = split + .next() + .with_context(|| format!("line {i} does not have value"))? + .split('#'); + let value = value_split + .next() + .with_context(|| format!("line {i} does not have value"))? + .trim() + .to_string(); + anyhow::Ok(EnvironmentVar { variable, value }) + }) + .collect::>>()?; + Ok(res) +} + +pub fn env_vars_deserializer<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(EnvironmentVarVisitor) +} + +pub fn option_env_vars_deserializer<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_any(OptionEnvVarVisitor) +} + +struct EnvironmentVarVisitor; + +impl<'de> Visitor<'de> for EnvironmentVarVisitor { + type Value = Vec; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter, + ) -> std::fmt::Result { + write!(formatter, "string or Vec") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + environment_vars_from_str(v) + .map_err(|e| serde::de::Error::custom(format!("{e:#}"))) + } + + fn visit_seq(self, seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + #[derive(Deserialize)] + struct EnvironmentVarInner { + variable: String, + value: String, + } + + impl From for EnvironmentVar { + fn from(value: EnvironmentVarInner) -> Self { + Self { + variable: value.variable, + value: value.value, + } + } + } + + let res = Vec::::deserialize( + SeqAccessDeserializer::new(seq), + )? + .into_iter() + .map(Into::into) + .collect(); + Ok(res) + } +} + +struct OptionEnvVarVisitor; + +impl<'de> Visitor<'de> for OptionEnvVarVisitor { + type Value = Option>; + + fn expecting( + &self, + formatter: &mut std::fmt::Formatter, + ) -> std::fmt::Result { + write!(formatter, "null or string or Vec") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + EnvironmentVarVisitor.visit_str(v).map(Some) + } + + fn visit_seq(self, seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + EnvironmentVarVisitor.visit_seq(seq).map(Some) + } + + fn visit_none(self) -> Result + where + E: serde::de::Error, + { + Ok(None) + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LatestCommit { pub hash: String,