platz-chart-ext 0.7.0

Platz Helm chart extensions
Documentation
use super::actions::ChartExtActions;
use super::features::ChartExtFeatures;
use super::ui_schema::UiSchema;
use crate::metadata::ChartMetadata;
use crate::resource_types::ChartExtResourceTypes;
use serde::{de::DeserializeOwned, Serialize};
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use tokio::fs::{self, read_to_string};
use tokio::try_join;

#[derive(Debug)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct ChartExt {
    pub metadata: Option<ChartMetadata>,
    pub ui_schema: Option<UiSchema>,
    pub actions: Option<ChartExtActions>,
    pub features: Option<ChartExtFeatures>,
    pub resource_types: Option<ChartExtResourceTypes>,
    pub error: Option<String>,
}

impl ChartExt {
    pub async fn from_path(path: &Path) -> Result<Self, std::io::Error> {
        match read_chart(path).await {
            Ok((metadata, ui_schema, actions, features, resource_types)) => Ok(Self {
                metadata: Some(metadata),
                ui_schema,
                actions,
                features,
                resource_types,
                error: None,
            }),
            Err(ChartExtError::IoError(err)) => Err(err),
            Err(error) => Ok(Self {
                metadata: None,
                ui_schema: None,
                actions: None,
                features: None,
                resource_types: None,
                error: Some(error.to_string()),
            }),
        }
    }

    pub fn new_with_error(error: String) -> Self {
        Self {
            metadata: None,
            ui_schema: None,
            actions: None,
            features: None,
            resource_types: None,
            error: Some(error),
        }
    }
}

#[derive(Debug, thiserror::Error)]
enum ChartExtError {
    #[error("std::io::Error: {0}")]
    IoError(#[from] std::io::Error),
    #[error("Could not find Chart.yaml in {0}")]
    NoChartYaml(PathBuf),
    #[error("Error while parsing {0}: {1}")]
    ParseError(String, String),
}

async fn platz_dir_exists(path: &Path) -> Result<bool, std::io::Error> {
    match fs::metadata(path.join("platz")).await {
        Ok(metadata) if metadata.is_dir() => Ok(true),
        Ok(_) => Ok(false),
        Err(err) if err.kind() == ErrorKind::NotFound => Ok(false),
        Err(err) => Err(err),
    }
}

async fn read_chart(
    path: &Path,
) -> Result<
    (
        ChartMetadata,
        Option<UiSchema>,
        Option<ChartExtActions>,
        Option<ChartExtFeatures>,
        Option<ChartExtResourceTypes>,
    ),
    ChartExtError,
> {
    let metadata = try_read_chart_metadata(path).await?;
    let (ui_schema, actions, features, resource_types) = if platz_dir_exists(path).await? {
        try_read_chart_extensions(
            path,
            Some("platz/values-ui.yaml"),
            Some("platz/actions.yaml"),
            Some("platz/features.yaml"),
            Some("platz/resources.yaml"),
        )
        .await?
    } else {
        try_read_chart_extensions(
            path,
            Some("values.ui.json"),
            Some("actions.schema.json"),
            Some("features.json"),
            None,
        )
        .await?
    };
    Ok((metadata, ui_schema, actions, features, resource_types))
}

async fn try_read_chart_metadata(chart_path: &Path) -> Result<ChartMetadata, ChartExtError> {
    read_spec_file(chart_path, Some("Chart.yaml"))
        .await?
        .ok_or_else(|| ChartExtError::NoChartYaml(chart_path.into()))
}

async fn try_read_chart_extensions(
    chart_path: &Path,
    ui_schema_filename: Option<&str>,
    actions_filename: Option<&str>,
    features_filename: Option<&str>,
    resource_types_filename: Option<&str>,
) -> Result<
    (
        Option<UiSchema>,
        Option<ChartExtActions>,
        Option<ChartExtFeatures>,
        Option<ChartExtResourceTypes>,
    ),
    ChartExtError,
> {
    Ok(try_join!(
        read_spec_file(chart_path, ui_schema_filename),
        read_spec_file(chart_path, actions_filename),
        read_spec_file(chart_path, features_filename),
        read_spec_file(chart_path, resource_types_filename),
    )?)
}

async fn read_spec_file<T>(path: &Path, filename: Option<&str>) -> Result<Option<T>, ChartExtError>
where
    T: Serialize + DeserializeOwned,
{
    let Some(filename) = filename else {
        return Ok(None)
    };

    let full_path = path.join(filename);

    let file_ext = full_path
        .extension()
        .and_then(|osstr| osstr.to_str())
        .map(ToString::to_string);

    let contents = match read_to_string(full_path).await {
        Ok(contents) => contents,
        Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None),
        Err(err) => return Err(err.into()),
    };

    match file_ext.as_deref() {
        Some("yaml") | Some("yml") => {
            Ok(Some(serde_yaml::from_str(&contents).map_err(|err| {
                ChartExtError::ParseError(filename.to_owned(), err.to_string())
            })?))
        }
        Some("json") => Ok(Some(serde_json::from_str(&contents).map_err(|err| {
            ChartExtError::ParseError(filename.to_owned(), err.to_string())
        })?)),
        _ => Err(ChartExtError::ParseError(
            filename.to_owned(),
            "Unknown file extension".to_owned(),
        )),
    }
}