1use std::path::PathBuf;
7use thiserror::Error;
8
9#[derive(Debug, Error)]
11pub enum ConvertError {
12 #[error("IO error: {0}")]
13 Io(#[from] std::io::Error),
14
15 #[error("Failed to parse Chart.yaml: {0}")]
16 ChartParse(#[from] crate::chart::ChartError),
17
18 #[error("Failed to parse template: {0}")]
19 TemplateParse(#[from] crate::parser::ParseError),
20
21 #[error("YAML error: {0}")]
22 Yaml(#[from] serde_yaml::Error),
23
24 #[error("Invalid chart: {0}")]
25 InvalidChart(String),
26
27 #[error("File not found: {0}")]
28 FileNotFound(PathBuf),
29
30 #[error("Directory not found: {0}")]
31 DirectoryNotFound(PathBuf),
32
33 #[error("Not a Helm chart: missing {0}")]
34 NotAChart(String),
35
36 #[error("Output directory already exists: {0}")]
37 OutputExists(PathBuf),
38
39 #[error("Conversion failed for {file}: {message}")]
40 ConversionFailed { file: PathBuf, message: String },
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
49pub enum WarningSeverity {
50 Info,
52 Warning,
54 Unsupported,
56 Error,
58}
59
60impl WarningSeverity {
61 pub fn color(&self) -> &'static str {
63 match self {
64 Self::Info => "cyan",
65 Self::Warning => "yellow",
66 Self::Unsupported => "magenta",
67 Self::Error => "red",
68 }
69 }
70
71 pub fn icon(&self) -> &'static str {
73 match self {
74 Self::Info => "ℹ",
75 Self::Warning => "⚠",
76 Self::Unsupported => "✗",
77 Self::Error => "✗",
78 }
79 }
80
81 pub fn label(&self) -> &'static str {
83 match self {
84 Self::Info => "info",
85 Self::Warning => "warning",
86 Self::Unsupported => "unsupported",
87 Self::Error => "error",
88 }
89 }
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94pub enum WarningCategory {
95 Syntax,
97 UnsupportedFeature,
99 Deprecated,
101 Security,
103 GitOps,
105 Performance,
107}
108
109impl WarningCategory {
110 pub fn label(&self) -> &'static str {
112 match self {
113 Self::Syntax => "syntax",
114 Self::UnsupportedFeature => "unsupported",
115 Self::Deprecated => "deprecated",
116 Self::Security => "security",
117 Self::GitOps => "gitops",
118 Self::Performance => "performance",
119 }
120 }
121}
122
123#[derive(Debug, Clone)]
125pub struct ConversionWarning {
126 pub severity: WarningSeverity,
128 pub category: WarningCategory,
130 pub file: PathBuf,
132 pub line: Option<usize>,
134 pub pattern: String,
136 pub message: String,
138 pub suggestion: Option<String>,
140 pub doc_link: Option<String>,
142}
143
144impl ConversionWarning {
145 pub fn info(file: PathBuf, pattern: &str, message: &str) -> Self {
147 Self {
148 severity: WarningSeverity::Info,
149 category: WarningCategory::Syntax,
150 file,
151 line: None,
152 pattern: pattern.to_string(),
153 message: message.to_string(),
154 suggestion: None,
155 doc_link: None,
156 }
157 }
158
159 pub fn warning(file: PathBuf, pattern: &str, message: &str) -> Self {
161 Self {
162 severity: WarningSeverity::Warning,
163 category: WarningCategory::Syntax,
164 file,
165 line: None,
166 pattern: pattern.to_string(),
167 message: message.to_string(),
168 suggestion: None,
169 doc_link: None,
170 }
171 }
172
173 pub fn unsupported(file: PathBuf, pattern: &str, alternative: &str) -> Self {
175 Self {
176 severity: WarningSeverity::Unsupported,
177 category: WarningCategory::UnsupportedFeature,
178 file,
179 line: None,
180 pattern: pattern.to_string(),
181 message: format!("'{}' is not supported in Sherpack", pattern),
182 suggestion: Some(alternative.to_string()),
183 doc_link: Some("https://sherpack.dev/docs/helm-migration".to_string()),
184 }
185 }
186
187 pub fn security(file: PathBuf, pattern: &str, message: &str, alternative: &str) -> Self {
189 Self {
190 severity: WarningSeverity::Unsupported,
191 category: WarningCategory::Security,
192 file,
193 line: None,
194 pattern: pattern.to_string(),
195 message: message.to_string(),
196 suggestion: Some(alternative.to_string()),
197 doc_link: Some("https://sherpack.dev/docs/security-best-practices".to_string()),
198 }
199 }
200
201 pub fn gitops(file: PathBuf, pattern: &str, message: &str, alternative: &str) -> Self {
203 Self {
204 severity: WarningSeverity::Warning,
205 category: WarningCategory::GitOps,
206 file,
207 line: None,
208 pattern: pattern.to_string(),
209 message: message.to_string(),
210 suggestion: Some(alternative.to_string()),
211 doc_link: Some("https://sherpack.dev/docs/gitops-compatibility".to_string()),
212 }
213 }
214
215 pub fn at_line(mut self, line: usize) -> Self {
217 self.line = Some(line);
218 self
219 }
220
221 pub fn with_suggestion(mut self, suggestion: &str) -> Self {
223 self.suggestion = Some(suggestion.to_string());
224 self
225 }
226
227 pub fn with_doc_link(mut self, url: &str) -> Self {
229 self.doc_link = Some(url.to_string());
230 self
231 }
232
233 pub fn with_category(mut self, category: WarningCategory) -> Self {
235 self.category = category;
236 self
237 }
238}
239
240impl std::fmt::Display for ConversionWarning {
241 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242 write!(f, "[{}] ", self.severity.label())?;
244 write!(f, "{}", self.file.display())?;
245
246 if let Some(line) = self.line {
247 write!(f, ":{}", line)?;
248 }
249
250 write!(f, " - {}", self.message)?;
251
252 if let Some(ref suggestion) = self.suggestion {
253 write!(f, "\n {} {}", self.severity.icon(), suggestion)?;
254 }
255
256 if let Some(ref link) = self.doc_link {
257 write!(f, "\n See: {}", link)?;
258 }
259
260 Ok(())
261 }
262}
263
264#[derive(Debug, Default)]
270pub struct ConversionSummary {
271 pub files_converted: usize,
273 pub files_copied: usize,
275 pub files_skipped: usize,
277 pub warnings: Vec<ConversionWarning>,
279}
280
281impl ConversionSummary {
282 pub fn new() -> Self {
284 Self::default()
285 }
286
287 pub fn add_warning(&mut self, warning: ConversionWarning) {
289 self.warnings.push(warning);
290 }
291
292 pub fn warnings_by_severity(
294 &self,
295 ) -> std::collections::HashMap<WarningSeverity, Vec<&ConversionWarning>> {
296 let mut grouped = std::collections::HashMap::new();
297 for warning in &self.warnings {
298 grouped
299 .entry(warning.severity)
300 .or_insert_with(Vec::new)
301 .push(warning);
302 }
303 grouped
304 }
305
306 pub fn warnings_by_category(
308 &self,
309 ) -> std::collections::HashMap<WarningCategory, Vec<&ConversionWarning>> {
310 let mut grouped = std::collections::HashMap::new();
311 for warning in &self.warnings {
312 grouped
313 .entry(warning.category)
314 .or_insert_with(Vec::new)
315 .push(warning);
316 }
317 grouped
318 }
319
320 pub fn count_by_severity(&self, severity: WarningSeverity) -> usize {
322 self.warnings
323 .iter()
324 .filter(|w| w.severity == severity)
325 .count()
326 }
327
328 pub fn has_errors(&self) -> bool {
330 self.warnings
331 .iter()
332 .any(|w| w.severity == WarningSeverity::Error)
333 }
334
335 pub fn has_unsupported(&self) -> bool {
337 self.warnings
338 .iter()
339 .any(|w| w.severity == WarningSeverity::Unsupported)
340 }
341
342 pub fn success_message(&self) -> String {
344 let mut msg = format!(
345 "Converted {} file{}, copied {} file{}",
346 self.files_converted,
347 if self.files_converted == 1 { "" } else { "s" },
348 self.files_copied,
349 if self.files_copied == 1 { "" } else { "s" },
350 );
351
352 if self.files_skipped > 0 {
353 msg.push_str(&format!(", skipped {}", self.files_skipped));
354 }
355
356 let info_count = self.count_by_severity(WarningSeverity::Info);
357 let warning_count = self.count_by_severity(WarningSeverity::Warning);
358 let unsupported_count = self.count_by_severity(WarningSeverity::Unsupported);
359
360 if info_count + warning_count + unsupported_count > 0 {
361 msg.push_str(&format!(
362 " with {} warning{}",
363 info_count + warning_count + unsupported_count,
364 if info_count + warning_count + unsupported_count == 1 {
365 ""
366 } else {
367 "s"
368 }
369 ));
370 }
371
372 msg
373 }
374}
375
376pub type Result<T> = std::result::Result<T, ConvertError>;
378
379pub mod warnings {
385 use super::*;
386 use std::path::Path;
387
388 pub fn crypto_in_template(file: &Path, func_name: &str) -> ConversionWarning {
390 ConversionWarning::security(
391 file.to_path_buf(),
392 func_name,
393 &format!(
394 "'{}' generates cryptographic material in templates - this is insecure",
395 func_name
396 ),
397 "Use cert-manager for certificates or external-secrets for keys",
398 )
399 }
400
401 pub fn files_access(file: &Path, method: &str) -> ConversionWarning {
403 ConversionWarning::unsupported(
404 file.to_path_buf(),
405 &format!(".Files.{}", method),
406 "Embed file content in values.yaml or use ConfigMap/Secret resources",
407 )
408 .with_category(WarningCategory::UnsupportedFeature)
409 }
410
411 pub fn lookup_function(file: &Path) -> ConversionWarning {
413 ConversionWarning::gitops(
414 file.to_path_buf(),
415 "lookup",
416 "'lookup' queries the cluster at render time - incompatible with GitOps",
417 "Returns {} in template mode. Use explicit values for GitOps compatibility.",
418 )
419 }
420
421 pub fn dynamic_tpl(file: &Path) -> ConversionWarning {
423 ConversionWarning::warning(
424 file.to_path_buf(),
425 "tpl",
426 "'tpl' with dynamic input may have security implications",
427 )
428 .with_suggestion("Sherpack limits tpl recursion depth to 10 for safety")
429 .with_doc_link("https://sherpack.dev/docs/template-security")
430 }
431
432 pub fn dns_lookup(file: &Path) -> ConversionWarning {
434 ConversionWarning::gitops(
435 file.to_path_buf(),
436 "getHostByName",
437 "'getHostByName' performs DNS lookup at render time - non-deterministic",
438 "Use explicit IP address or hostname in values.yaml",
439 )
440 }
441
442 pub fn random_function(file: &Path, func_name: &str) -> ConversionWarning {
444 ConversionWarning::gitops(
445 file.to_path_buf(),
446 func_name,
447 &format!(
448 "'{}' generates different values on each render - breaks GitOps",
449 func_name
450 ),
451 "Pre-generate values and store in values.yaml or use external-secrets",
452 )
453 }
454
455 pub fn syntax_converted(file: &Path, from: &str, to: &str) -> ConversionWarning {
457 ConversionWarning::info(
458 file.to_path_buf(),
459 from,
460 &format!("Converted '{}' to '{}'", from, to),
461 )
462 .with_category(WarningCategory::Syntax)
463 }
464
465 pub fn with_block_context(file: &Path) -> ConversionWarning {
467 ConversionWarning::warning(
468 file.to_path_buf(),
469 "with",
470 "'with' block context scoping differs between Go templates and Jinja2",
471 )
472 .with_suggestion("Review converted template - use explicit variable names if needed")
473 .with_category(WarningCategory::Syntax)
474 }
475
476 pub fn macro_converted(file: &Path, helm_name: &str, jinja_name: &str) -> ConversionWarning {
478 ConversionWarning::info(
479 file.to_path_buf(),
480 &format!("define \"{}\"", helm_name),
481 &format!("Converted to Jinja2 macro '{}'", jinja_name),
482 )
483 .with_category(WarningCategory::Syntax)
484 }
485}