use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ConvertError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Failed to parse Chart.yaml: {0}")]
ChartParse(#[from] crate::chart::ChartError),
#[error("Failed to parse template: {0}")]
TemplateParse(#[from] crate::parser::ParseError),
#[error("YAML error: {0}")]
Yaml(#[from] serde_yaml::Error),
#[error("Invalid chart: {0}")]
InvalidChart(String),
#[error("File not found: {0}")]
FileNotFound(PathBuf),
#[error("Directory not found: {0}")]
DirectoryNotFound(PathBuf),
#[error("Not a Helm chart: missing {0}")]
NotAChart(String),
#[error("Output directory already exists: {0}")]
OutputExists(PathBuf),
#[error("Conversion failed for {file}: {message}")]
ConversionFailed { file: PathBuf, message: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum WarningSeverity {
Info,
Warning,
Unsupported,
Error,
}
impl WarningSeverity {
pub fn color(&self) -> &'static str {
match self {
Self::Info => "cyan",
Self::Warning => "yellow",
Self::Unsupported => "magenta",
Self::Error => "red",
}
}
pub fn icon(&self) -> &'static str {
match self {
Self::Info => "ℹ",
Self::Warning => "⚠",
Self::Unsupported => "✗",
Self::Error => "✗",
}
}
pub fn label(&self) -> &'static str {
match self {
Self::Info => "info",
Self::Warning => "warning",
Self::Unsupported => "unsupported",
Self::Error => "error",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WarningCategory {
Syntax,
UnsupportedFeature,
Deprecated,
Security,
GitOps,
Performance,
}
impl WarningCategory {
pub fn label(&self) -> &'static str {
match self {
Self::Syntax => "syntax",
Self::UnsupportedFeature => "unsupported",
Self::Deprecated => "deprecated",
Self::Security => "security",
Self::GitOps => "gitops",
Self::Performance => "performance",
}
}
}
#[derive(Debug, Clone)]
pub struct ConversionWarning {
pub severity: WarningSeverity,
pub category: WarningCategory,
pub file: PathBuf,
pub line: Option<usize>,
pub pattern: String,
pub message: String,
pub suggestion: Option<String>,
pub doc_link: Option<String>,
}
impl ConversionWarning {
pub fn info(file: PathBuf, pattern: &str, message: &str) -> Self {
Self {
severity: WarningSeverity::Info,
category: WarningCategory::Syntax,
file,
line: None,
pattern: pattern.to_string(),
message: message.to_string(),
suggestion: None,
doc_link: None,
}
}
pub fn warning(file: PathBuf, pattern: &str, message: &str) -> Self {
Self {
severity: WarningSeverity::Warning,
category: WarningCategory::Syntax,
file,
line: None,
pattern: pattern.to_string(),
message: message.to_string(),
suggestion: None,
doc_link: None,
}
}
pub fn unsupported(file: PathBuf, pattern: &str, alternative: &str) -> Self {
Self {
severity: WarningSeverity::Unsupported,
category: WarningCategory::UnsupportedFeature,
file,
line: None,
pattern: pattern.to_string(),
message: format!("'{}' is not supported in Sherpack", pattern),
suggestion: Some(alternative.to_string()),
doc_link: Some("https://sherpack.dev/docs/helm-migration".to_string()),
}
}
pub fn security(file: PathBuf, pattern: &str, message: &str, alternative: &str) -> Self {
Self {
severity: WarningSeverity::Unsupported,
category: WarningCategory::Security,
file,
line: None,
pattern: pattern.to_string(),
message: message.to_string(),
suggestion: Some(alternative.to_string()),
doc_link: Some("https://sherpack.dev/docs/security-best-practices".to_string()),
}
}
pub fn gitops(file: PathBuf, pattern: &str, message: &str, alternative: &str) -> Self {
Self {
severity: WarningSeverity::Warning,
category: WarningCategory::GitOps,
file,
line: None,
pattern: pattern.to_string(),
message: message.to_string(),
suggestion: Some(alternative.to_string()),
doc_link: Some("https://sherpack.dev/docs/gitops-compatibility".to_string()),
}
}
pub fn at_line(mut self, line: usize) -> Self {
self.line = Some(line);
self
}
pub fn with_suggestion(mut self, suggestion: &str) -> Self {
self.suggestion = Some(suggestion.to_string());
self
}
pub fn with_doc_link(mut self, url: &str) -> Self {
self.doc_link = Some(url.to_string());
self
}
pub fn with_category(mut self, category: WarningCategory) -> Self {
self.category = category;
self
}
}
impl std::fmt::Display for ConversionWarning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] ", self.severity.label())?;
write!(f, "{}", self.file.display())?;
if let Some(line) = self.line {
write!(f, ":{}", line)?;
}
write!(f, " - {}", self.message)?;
if let Some(ref suggestion) = self.suggestion {
write!(f, "\n {} {}", self.severity.icon(), suggestion)?;
}
if let Some(ref link) = self.doc_link {
write!(f, "\n See: {}", link)?;
}
Ok(())
}
}
#[derive(Debug, Default)]
pub struct ConversionSummary {
pub files_converted: usize,
pub files_copied: usize,
pub files_skipped: usize,
pub warnings: Vec<ConversionWarning>,
}
impl ConversionSummary {
pub fn new() -> Self {
Self::default()
}
pub fn add_warning(&mut self, warning: ConversionWarning) {
self.warnings.push(warning);
}
pub fn warnings_by_severity(
&self,
) -> std::collections::HashMap<WarningSeverity, Vec<&ConversionWarning>> {
let mut grouped = std::collections::HashMap::new();
for warning in &self.warnings {
grouped
.entry(warning.severity)
.or_insert_with(Vec::new)
.push(warning);
}
grouped
}
pub fn warnings_by_category(
&self,
) -> std::collections::HashMap<WarningCategory, Vec<&ConversionWarning>> {
let mut grouped = std::collections::HashMap::new();
for warning in &self.warnings {
grouped
.entry(warning.category)
.or_insert_with(Vec::new)
.push(warning);
}
grouped
}
pub fn count_by_severity(&self, severity: WarningSeverity) -> usize {
self.warnings
.iter()
.filter(|w| w.severity == severity)
.count()
}
pub fn has_errors(&self) -> bool {
self.warnings
.iter()
.any(|w| w.severity == WarningSeverity::Error)
}
pub fn has_unsupported(&self) -> bool {
self.warnings
.iter()
.any(|w| w.severity == WarningSeverity::Unsupported)
}
pub fn success_message(&self) -> String {
let mut msg = format!(
"Converted {} file{}, copied {} file{}",
self.files_converted,
if self.files_converted == 1 { "" } else { "s" },
self.files_copied,
if self.files_copied == 1 { "" } else { "s" },
);
if self.files_skipped > 0 {
msg.push_str(&format!(", skipped {}", self.files_skipped));
}
let info_count = self.count_by_severity(WarningSeverity::Info);
let warning_count = self.count_by_severity(WarningSeverity::Warning);
let unsupported_count = self.count_by_severity(WarningSeverity::Unsupported);
if info_count + warning_count + unsupported_count > 0 {
msg.push_str(&format!(
" with {} warning{}",
info_count + warning_count + unsupported_count,
if info_count + warning_count + unsupported_count == 1 {
""
} else {
"s"
}
));
}
msg
}
}
pub type Result<T> = std::result::Result<T, ConvertError>;
pub mod warnings {
use super::*;
use std::path::Path;
pub fn crypto_in_template(file: &Path, func_name: &str) -> ConversionWarning {
ConversionWarning::security(
file.to_path_buf(),
func_name,
&format!(
"'{}' generates cryptographic material in templates - this is insecure",
func_name
),
"Use cert-manager for certificates or external-secrets for keys",
)
}
pub fn files_access(file: &Path, method: &str) -> ConversionWarning {
ConversionWarning::unsupported(
file.to_path_buf(),
&format!(".Files.{}", method),
"Embed file content in values.yaml or use ConfigMap/Secret resources",
)
.with_category(WarningCategory::UnsupportedFeature)
}
pub fn lookup_function(file: &Path) -> ConversionWarning {
ConversionWarning::gitops(
file.to_path_buf(),
"lookup",
"'lookup' queries the cluster at render time — same Pack rendered against different clusters produces different manifests",
"Preserved by the converter. Returns the live resource during `sherpack install/upgrade`, returns {} in `sherpack template`. For GitOps reproducibility, prefer explicit values + the existingSecret pattern.",
)
}
pub fn dynamic_tpl(file: &Path) -> ConversionWarning {
ConversionWarning::warning(
file.to_path_buf(),
"tpl",
"'tpl' with dynamic input may have security implications",
)
.with_suggestion("Sherpack limits tpl recursion depth to 10 for safety")
.with_doc_link("https://sherpack.dev/docs/template-security")
}
pub fn dns_lookup(file: &Path) -> ConversionWarning {
ConversionWarning::gitops(
file.to_path_buf(),
"getHostByName",
"'getHostByName' performs DNS lookup at render time - non-deterministic",
"Use explicit IP address or hostname in values.yaml",
)
}
pub fn random_function(file: &Path, func_name: &str) -> ConversionWarning {
ConversionWarning::gitops(
file.to_path_buf(),
func_name,
&format!(
"'{}' generates different values on each render - breaks GitOps",
func_name
),
"Pre-generate values and store in values.yaml or use external-secrets",
)
}
pub fn syntax_converted(file: &Path, from: &str, to: &str) -> ConversionWarning {
ConversionWarning::info(
file.to_path_buf(),
from,
&format!("Converted '{}' to '{}'", from, to),
)
.with_category(WarningCategory::Syntax)
}
pub fn with_block_context(file: &Path) -> ConversionWarning {
ConversionWarning::warning(
file.to_path_buf(),
"with",
"'with' block context scoping differs between Go templates and Jinja2",
)
.with_suggestion("Review converted template - use explicit variable names if needed")
.with_category(WarningCategory::Syntax)
}
pub fn macro_converted(file: &Path, helm_name: &str, jinja_name: &str) -> ConversionWarning {
ConversionWarning::info(
file.to_path_buf(),
&format!("define \"{}\"", helm_name),
&format!("Converted to Jinja2 macro '{}'", jinja_name),
)
.with_category(WarningCategory::Syntax)
}
}