use crate::dsh_api_client::DshApiClient;
use crate::epoch_milliseconds_to_string;
use crate::error::DshApiResult;
use crate::types::AppCatalogManifest;
use crate::version::Version;
#[allow(unused_imports)]
use crate::DshApiError;
use itertools::Itertools;
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::{from_str, Value};
use std::collections::{HashMap, HashSet};
use std::fmt::{Debug, Display, Formatter};
use std::str::FromStr;
impl DshApiClient {
pub async fn manifest(&self, manifest_id: &str, manifest_version: &Version) -> DshApiResult<Manifest> {
self
.manifests()
.await?
.iter()
.find(|manifest| manifest.id == manifest_id && manifest.version == *manifest_version)
.cloned()
.ok_or(DshApiError::not_found())
}
pub async fn manifest_latest_version(&self, manifest_id: &str, allow_draft_version: bool) -> DshApiResult<Manifest> {
match self.manifest_all_versions(manifest_id).await {
Ok(manifests) => match manifests.into_iter().filter(|manifest| !manifest.draft || allow_draft_version).next_back() {
Some(latest_manifest) => Ok(latest_manifest),
None => Err(DshApiError::not_found()),
},
Err(_) => Err(DshApiError::not_found()),
}
}
pub async fn manifest_all_versions(&self, manifest_id: &str) -> DshApiResult<Vec<Manifest>> {
let mut manifests: Vec<Manifest> = self.manifests().await?.into_iter().filter(|manifest| manifest.id == manifest_id).collect_vec();
if manifests.is_empty() {
Err(DshApiError::not_found())
} else {
manifests.sort_by(|manifest_a, manifest_b| manifest_a.version.cmp(&manifest_b.version));
Ok(manifests)
}
}
pub async fn manifests(&self) -> DshApiResult<Vec<Manifest>> {
self.get_appcatalog_manifests().await?.iter().map(Manifest::try_from).try_collect()
}
pub async fn manifest_raw(&self, manifest_id: &str, manifest_version: &Version) -> DshApiResult<(String, bool)> {
for app_catalog_manifest in self.get_appcatalog_manifests().await?.iter() {
let payload = from_str::<HashMap<String, Value>>(app_catalog_manifest.payload.as_str())?;
if payload.get("id").is_some_and(|payload_id| payload_id.as_str().unwrap() == manifest_id)
&& payload
.get("version")
.is_some_and(|version_value| Version::from_str(version_value.as_str().unwrap()).unwrap() == *manifest_version)
{
return Ok((serde_json::to_string_pretty(&payload)?, app_catalog_manifest.draft));
}
}
Err(DshApiError::not_found())
}
pub async fn manifest_raw_latest(&self, manifest_id: &str, allow_draft_version: bool) -> DshApiResult<(Version, String, bool)> {
let mut raw_manifests: Vec<(Version, bool, HashMap<String, Value>)> = self
.get_appcatalog_manifests()
.await?
.iter()
.filter(|manifest| !manifest.draft || allow_draft_version)
.filter_map(|manifest| match from_str::<HashMap<String, Value>>(manifest.payload.as_str()) {
Ok(payload) => {
if payload.get("id").is_some_and(|payload_id| payload_id.as_str().unwrap() == manifest_id) {
match payload.get("version").map(|version_value| Version::from_str(version_value.as_str().unwrap())) {
Some(Ok(version)) => Some((version, manifest.draft, payload)),
_ => None,
}
} else {
None
}
}
Err(_) => None,
})
.collect_vec();
raw_manifests.sort_by(|(version_a, _, _), (version_b, _, _)| version_a.cmp(version_b));
match raw_manifests.last() {
Some((last_version, draft, last_payload)) => Ok((last_version.clone(), serde_json::to_string_pretty(&last_payload)?, *draft)),
None => Err(DshApiError::not_found()),
}
}
pub async fn manifest_ids(&self) -> DshApiResult<Vec<String>> {
let mut ids: Vec<String> = self
.manifests()
.await?
.iter()
.map(|manifest| manifest.id.clone())
.collect::<HashSet<_>>()
.into_iter()
.collect();
ids.sort();
Ok(ids)
}
pub async fn manifest_ids_versions(&self) -> DshApiResult<Vec<(String, Vec<(Version, bool)>)>> {
let mut id_versions_map: HashMap<String, Vec<(Version, bool)>> = HashMap::new();
for manifest in self.manifests().await? {
id_versions_map
.entry(manifest.id)
.and_modify(|versions| versions.push((manifest.version.clone(), manifest.draft)))
.or_insert(vec![(manifest.version, manifest.draft)]);
}
let mut id_versions_pairs: Vec<(String, Vec<(Version, bool)>)> = id_versions_map.iter().map(|(id, versions)| (id.to_string(), versions.clone())).collect();
id_versions_pairs.sort_by(|(id_a, _), (id_b, _)| id_a.cmp(id_b));
for (_, versions) in id_versions_pairs.iter_mut() {
versions.sort();
}
Ok(id_versions_pairs)
}
pub async fn manifests_all_versions(&self) -> DshApiResult<Vec<(String, Vec<Manifest>)>> {
let mut id_manifests: Vec<(String, Vec<Manifest>)> = self
.manifests()
.await?
.into_iter()
.map(|manifest| (manifest.id.clone(), manifest))
.into_group_map()
.into_iter()
.collect_vec()
.into_iter()
.map(|(manifest_id, mut manifests)| {
manifests.sort_by(|manifest_a, manifest_b| manifest_a.version.cmp(&manifest_b.version));
(manifest_id, manifests)
})
.collect_vec();
id_manifests.sort_by(|(id_a, _), (id_b, _)| id_a.cmp(id_b));
Ok(id_manifests)
}
pub async fn manifests_latest_version(&self, allow_draft: bool) -> DshApiResult<Vec<(String, Manifest)>> {
let manifests_grouped_by_id: Vec<(String, Vec<Manifest>)> = self
.manifests()
.await?
.into_iter()
.map(|manifest| (manifest.id.clone(), manifest))
.collect_vec()
.into_iter()
.into_group_map()
.into_iter()
.collect_vec();
let mut latest_manifests: Vec<(String, Manifest)> = manifests_grouped_by_id
.into_iter()
.filter_map(|(id, manifests)| {
manifests
.into_iter()
.filter(|manifest| !manifest.draft || allow_draft)
.max_by_key(|manifest| manifest.version.clone())
.map(|manifest| (id, manifest))
})
.collect_vec();
latest_manifests.sort_by(|(id_a, _), (id_b, _)| id_a.cmp(id_b));
Ok(latest_manifests)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Manifest {
#[serde(skip_deserializing)]
pub draft: bool,
#[serde(skip_deserializing)]
pub last_modified: String,
pub id: String,
pub name: String,
pub version: Version,
pub vendor: String,
pub kind: Option<String>,
#[serde(rename = "apiVersion")]
pub api_version: Option<String>,
pub description: Option<String>,
#[serde(rename = "moreInfo")]
pub more_info: Option<String>,
pub contact: String,
pub configuration: Option<Configuration>,
#[serde(deserialize_with = "deserialize_resource_map")]
pub resources: HashMap<String, Resource>,
}
impl Display for Manifest {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.id, self.version)
}
}
impl TryFrom<&AppCatalogManifest> for Manifest {
type Error = DshApiError;
fn try_from(app_catalog_manifest: &AppCatalogManifest) -> Result<Self, Self::Error> {
from_str::<Manifest>(app_catalog_manifest.payload.as_str())
.map(|payload| Manifest { draft: app_catalog_manifest.draft, last_modified: epoch_milliseconds_to_string(app_catalog_manifest.last_modified as i64), ..payload })
.map_err(DshApiError::from)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Configuration {
#[serde(rename = "$schema")]
pub schema: String,
#[serde(rename = "type")]
pub kind: String,
pub properties: HashMap<String, Property>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Property {
pub description: String,
#[serde(rename = "type")]
pub kind: PropertyKind,
#[serde(rename = "enum")]
pub enumeration: Option<Vec<String>>,
pub default: Option<String>,
}
impl Display for Property {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
fn display_enumeration(enumeration: &[String], default: Option<&String>) -> String {
enumeration
.iter()
.map(
|enumeration_value| {
if default.is_some_and(|default_value| default_value == enumeration_value) {
format!("{}*", enumeration_value)
} else {
enumeration_value.to_string()
}
},
)
.join("|")
}
match self.kind {
PropertyKind::DnsZone => write!(f, "dns-zone"),
PropertyKind::Number => {
if let Some(enumeration) = &self.enumeration {
write!(f, "number:{}", display_enumeration(enumeration, Option::from(&self.default)))
} else if let Some(default_value) = &self.default {
write!(f, "number:default={}", default_value)
} else {
write!(f, "number")
}
}
PropertyKind::String => {
if let Some(enumeration) = &self.enumeration {
write!(f, "string:{}", display_enumeration(enumeration, Option::from(&self.default)))
} else if let Some(default_value) = &self.default {
write!(f, "string:default=\"{}\"", default_value)
} else {
write!(f, "string")
}
}
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub enum PropertyKind {
#[serde(rename = "dns-zone")]
DnsZone,
#[serde(rename = "number")]
Number,
#[serde(rename = "string")]
String,
}
impl Display for PropertyKind {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self {
PropertyKind::DnsZone => write!(f, "dns-zone"),
PropertyKind::Number => write!(f, "number"),
PropertyKind::String => write!(f, "string"),
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub enum Resource {
Application { application: Box<ApplicationResource> },
Bucket { bucket: Box<BucketResource> },
Certificate { certificate: Box<CertificateResource> },
Database { database: Box<DatabaseResource> },
Secret { secret: Box<SecretResource> },
Topic { topic: Box<TopicResource> },
Vhost { vhost: Box<VhostResource> },
Volume { volume: Box<VolumeResource> },
}
impl Display for Resource {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Application { application } => Display::fmt(&application, f),
Self::Bucket { bucket } => Display::fmt(&bucket, f),
Self::Certificate { certificate } => Display::fmt(&certificate, f),
Self::Database { database } => Display::fmt(&database, f),
Self::Secret { secret } => Display::fmt(&secret, f),
Self::Topic { topic } => Display::fmt(&topic, f),
Self::Vhost { vhost } => Display::fmt(&vhost, f),
Self::Volume { volume } => Display::fmt(&volume, f),
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(try_from = "Value", into = "Value")]
pub enum Numerical {
Float { value: f64 },
Integer { value: i64 },
Template { template: String },
}
impl Display for Numerical {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Float { value } => write!(f, "{}", value),
Self::Integer { value } => write!(f, "{}", value),
Self::Template { template } => write!(f, "{}", template),
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ApplicationResource {
pub cpus: Numerical,
pub env: HashMap<String, String>,
#[serde(rename = "exposedPorts")]
pub exposed_ports: Option<HashMap<String, ExposedPort>>,
pub image: String,
#[serde(rename = "imageConsole")]
pub image_console: Option<String>,
pub instances: Numerical,
pub mem: Numerical,
pub metrics: Option<Metrics>,
pub name: String,
#[serde(rename = "needsToken")]
pub needs_token: bool,
pub secrets: Option<Vec<Secret>>,
#[serde(rename = "singleInstance")]
pub single_instance: bool,
pub user: String,
}
impl Display for ApplicationResource {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.name, self.image)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct BucketResource {
pub encrypted: bool,
pub name: String,
pub versioned: bool,
}
impl Display for BucketResource {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)?;
if self.encrypted {
write!(f, ":encrypted")?;
}
if self.versioned {
write!(f, ":versioned")?;
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct CertificateResource {
pub unformatted_representation: String,
}
impl Display for CertificateResource {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.unformatted_representation)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct DatabaseResource {
pub cpus: Numerical,
pub extensions: Vec<String>,
pub instances: Numerical,
pub mem: Numerical,
pub name: String,
#[serde(rename = "snapshotInterval")]
pub snapshot_interval: Numerical,
pub version: String,
#[serde(rename = "volumeSize")]
pub volume_size: Numerical,
}
impl Display for DatabaseResource {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.name, self.version)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct SecretResource {
pub unformatted_representation: String,
}
impl Display for SecretResource {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.unformatted_representation)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct TopicResource {
#[serde(rename = "kafkaProperties")]
pub kafka_properties: Option<HashMap<String, String>>,
pub name: String,
pub partitions: i64,
#[serde(rename = "replicationFactor")]
pub replication_factor: i64,
}
impl Display for TopicResource {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}:{}", self.name, self.partitions, self.replication_factor)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct VhostResource {
pub unformatted_representation: String,
}
impl Display for VhostResource {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.unformatted_representation)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct VolumeResource {
pub name: String,
pub size: Numerical,
}
impl Display for VolumeResource {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.name, self.size)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ExposedPort {
pub auth: Option<String>,
pub tls: Option<String>,
pub vhost: String,
}
impl Display for ExposedPort {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.vhost)?;
if let Some(auth) = &self.auth {
write!(f, ":{}", auth)?;
}
if let Some(tls) = &self.tls {
write!(f, ":{}", tls)?;
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Injection {
pub env: String,
}
impl Display for Injection {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.env)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Secret {
pub injections: Vec<Injection>,
pub name: String,
}
impl Display for Secret {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)?;
if self.injections.is_empty() {
write!(f, ":{}", self.injections.iter().map(|injection| injection.to_string()).join(","))?;
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Metrics {
pub path: String,
pub port: i64,
}
impl Display for Metrics {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.path, self.port)
}
}
fn deserialize_resource_map<'de, D>(deserializer: D) -> Result<HashMap<String, Resource>, D::Error>
where
D: Deserializer<'de>,
{
HashMap::<String, Value>::deserialize(deserializer).and_then(|deserialized_map| {
deserialized_map
.iter()
.map(|(key, value)| {
let key_parts = key.split("/").collect_vec();
match key_parts.get(2) {
Some(resource_type) => match *resource_type {
"application" => Resource::application(value),
"bucket" => Resource::bucket(value),
"certificate" => Resource::certificate(value),
"database" => Resource::database(value),
"secret" => Resource::secret(value),
"topic" => Resource::topic(value),
"vhost" => Resource::vhost(value),
"volume" => Resource::volume(value),
unknown_resource => Err(serde_json::Error::custom(format!("unknown resource type ({})", unknown_resource))),
}
.map(|resource| (key.to_string(), resource)),
None => Err(serde_json::Error::custom(format!("illegal resource allocation ({})", key))),
}
})
.try_collect()
.map_err(D::Error::custom)
})
}
impl Resource {
fn application(value: &Value) -> Result<Resource, serde_json::Error> {
ApplicationResource::deserialize(value).map(|application_resource| Resource::Application { application: Box::new(application_resource) })
}
fn bucket(value: &Value) -> Result<Resource, serde_json::Error> {
BucketResource::deserialize(value).map(|bucket_resource| Resource::Bucket { bucket: Box::new(bucket_resource) })
}
fn certificate(value: &Value) -> Result<Resource, serde_json::Error> {
Ok(Resource::Certificate { certificate: Box::new(CertificateResource { unformatted_representation: value.to_string() }) })
}
fn database(value: &Value) -> Result<Resource, serde_json::Error> {
DatabaseResource::deserialize(value).map(|database_resource| Resource::Database { database: Box::new(database_resource) })
}
fn secret(value: &Value) -> Result<Resource, serde_json::Error> {
Ok(Resource::Secret { secret: Box::new(SecretResource { unformatted_representation: value.to_string() }) })
}
fn topic(value: &Value) -> Result<Resource, serde_json::Error> {
TopicResource::deserialize(value).map(|topic_resource| Resource::Topic { topic: Box::new(topic_resource) })
}
fn vhost(value: &Value) -> Result<Resource, serde_json::Error> {
Ok(Resource::Vhost { vhost: Box::new(VhostResource { unformatted_representation: value.to_string() }) })
}
fn volume(value: &Value) -> Result<Resource, serde_json::Error> {
VolumeResource::deserialize(value).map(|volume_resource| Resource::Volume { volume: Box::new(volume_resource) })
}
}
impl TryFrom<Value> for Numerical {
type Error = String;
fn try_from(value: Value) -> Result<Self, Self::Error> {
match value.as_i64() {
Some(value) => Ok(Numerical::Integer { value }),
None => match value.as_f64() {
Some(value) => Ok(Numerical::Float { value }),
None => match value.as_str() {
Some(template) => Ok(Numerical::Template { template: template.to_string() }),
None => Err(format!("could not parse '{}' value", value)),
},
},
}
}
}
impl From<Numerical> for Value {
fn from(numerical: Numerical) -> Self {
match numerical {
Numerical::Float { value } => Value::from(value),
Numerical::Integer { value } => Value::from(value),
Numerical::Template { template } => Value::from(template),
}
}
}