1use super::actions::ChartExtActions;
2use super::features::ChartExtFeatures;
3use super::ui_schema::UiSchema;
4use crate::metadata::ChartMetadata;
5use crate::resource_types::ChartExtResourceTypes;
6use serde::{de::DeserializeOwned, Serialize};
7use std::io::ErrorKind;
8use std::path::{Path, PathBuf};
9use tokio::fs::{self, read_to_string};
10use tokio::try_join;
11
12#[derive(Debug)]
13#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
14pub struct ChartExt {
15 pub metadata: Option<ChartMetadata>,
16 pub ui_schema: Option<UiSchema>,
17 pub actions: Option<ChartExtActions>,
18 pub features: Option<ChartExtFeatures>,
19 pub resource_types: Option<ChartExtResourceTypes>,
20 pub error: Option<String>,
21}
22
23impl ChartExt {
24 pub async fn from_path(path: &Path) -> Result<Self, std::io::Error> {
25 match read_chart(path).await {
26 Ok((metadata, ui_schema, actions, features, resource_types)) => Ok(Self {
27 metadata: Some(metadata),
28 ui_schema,
29 actions,
30 features,
31 resource_types,
32 error: None,
33 }),
34 Err(ChartExtError::IoError(err)) => Err(err),
35 Err(error) => Ok(Self {
36 metadata: None,
37 ui_schema: None,
38 actions: None,
39 features: None,
40 resource_types: None,
41 error: Some(error.to_string()),
42 }),
43 }
44 }
45
46 pub fn new_with_error(error: String) -> Self {
47 Self {
48 metadata: None,
49 ui_schema: None,
50 actions: None,
51 features: None,
52 resource_types: None,
53 error: Some(error),
54 }
55 }
56}
57
58#[derive(Debug, thiserror::Error)]
59enum ChartExtError {
60 #[error("std::io::Error: {0}")]
61 IoError(#[from] std::io::Error),
62 #[error("Could not find Chart.yaml in {0}")]
63 NoChartYaml(PathBuf),
64 #[error("Error while parsing {0}: {1}")]
65 ParseError(String, String),
66}
67
68async fn platz_dir_exists(path: &Path) -> Result<bool, std::io::Error> {
69 match fs::metadata(path.join("platz")).await {
70 Ok(metadata) if metadata.is_dir() => Ok(true),
71 Ok(_) => Ok(false),
72 Err(err) if err.kind() == ErrorKind::NotFound => Ok(false),
73 Err(err) => Err(err),
74 }
75}
76
77async fn read_chart(
78 path: &Path,
79) -> Result<
80 (
81 ChartMetadata,
82 Option<UiSchema>,
83 Option<ChartExtActions>,
84 Option<ChartExtFeatures>,
85 Option<ChartExtResourceTypes>,
86 ),
87 ChartExtError,
88> {
89 let metadata = try_read_chart_metadata(path).await?;
90 let (ui_schema, actions, features, resource_types) = if platz_dir_exists(path).await? {
91 try_read_chart_extensions(
92 path,
93 Some("platz/values-ui.yaml"),
94 Some("platz/actions.yaml"),
95 Some("platz/features.yaml"),
96 Some("platz/resources.yaml"),
97 )
98 .await?
99 } else {
100 try_read_chart_extensions(
101 path,
102 Some("values.ui.json"),
103 Some("actions.schema.json"),
104 Some("features.json"),
105 None,
106 )
107 .await?
108 };
109 Ok((metadata, ui_schema, actions, features, resource_types))
110}
111
112async fn try_read_chart_metadata(chart_path: &Path) -> Result<ChartMetadata, ChartExtError> {
113 read_spec_file(chart_path, Some("Chart.yaml"))
114 .await?
115 .ok_or_else(|| ChartExtError::NoChartYaml(chart_path.into()))
116}
117
118async fn try_read_chart_extensions(
119 chart_path: &Path,
120 ui_schema_filename: Option<&str>,
121 actions_filename: Option<&str>,
122 features_filename: Option<&str>,
123 resource_types_filename: Option<&str>,
124) -> Result<
125 (
126 Option<UiSchema>,
127 Option<ChartExtActions>,
128 Option<ChartExtFeatures>,
129 Option<ChartExtResourceTypes>,
130 ),
131 ChartExtError,
132> {
133 Ok(try_join!(
134 read_spec_file(chart_path, ui_schema_filename),
135 read_spec_file(chart_path, actions_filename),
136 read_spec_file(chart_path, features_filename),
137 read_spec_file(chart_path, resource_types_filename),
138 )?)
139}
140
141async fn read_spec_file<T>(path: &Path, filename: Option<&str>) -> Result<Option<T>, ChartExtError>
142where
143 T: Serialize + DeserializeOwned,
144{
145 let Some(filename) = filename else {
146 return Ok(None)
147 };
148
149 let full_path = path.join(filename);
150
151 let file_ext = full_path
152 .extension()
153 .and_then(|osstr| osstr.to_str())
154 .map(ToString::to_string);
155
156 let contents = match read_to_string(full_path).await {
157 Ok(contents) => contents,
158 Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None),
159 Err(err) => return Err(err.into()),
160 };
161
162 match file_ext.as_deref() {
163 Some("yaml") | Some("yml") => {
164 Ok(Some(serde_yaml::from_str(&contents).map_err(|err| {
165 ChartExtError::ParseError(filename.to_owned(), err.to_string())
166 })?))
167 }
168 Some("json") => Ok(Some(serde_json::from_str(&contents).map_err(|err| {
169 ChartExtError::ParseError(filename.to_owned(), err.to_string())
170 })?)),
171 _ => Err(ChartExtError::ParseError(
172 filename.to_owned(),
173 "Unknown file extension".to_owned(),
174 )),
175 }
176}