use std::collections::{BTreeMap, BTreeSet};
use std::path::{Component, Path};
use flate2::read::GzDecoder;
use serde::Deserialize;
use tracing::instrument;
use vfs::VfsPath;
use super::paths::scope_values_path;
use super::types::{ChartContext, ChartDependencyActivation};
use crate::error::{CliError, EngineResult};
use crate::load_budget::{LoadBudget, read_to_end_capped};
#[derive(Debug, Deserialize)]
struct ChartYaml {
name: Option<String>,
#[serde(rename = "type")]
chart_type: Option<String>,
dependencies: Option<Vec<ChartDependency>>,
}
#[derive(Debug, Deserialize)]
struct ChartDependency {
name: String,
alias: Option<String>,
condition: Option<String>,
tags: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
struct DependencyMetadata {
values_key: String,
activation: ChartDependencyActivation,
}
#[instrument(skip_all)]
pub fn discover_chart_contexts(root_chart_dir: &VfsPath) -> EngineResult<Vec<ChartContext>> {
discover_chart_contexts_with_budget(root_chart_dir, LoadBudget::default())
}
#[instrument(skip_all)]
pub(crate) fn discover_chart_contexts_with_budget(
root_chart_dir: &VfsPath,
load_budget: LoadBudget,
) -> EngineResult<Vec<ChartContext>> {
let mut out = Vec::new();
discover_chart_contexts_inner(root_chart_dir, &[], &[], load_budget, &mut out)?;
Ok(out)
}
fn discover_chart_contexts_inner(
chart_dir: &VfsPath,
parent_prefix: &[String],
dependency_activation_chain: &[ChartDependencyActivation],
load_budget: LoadBudget,
out: &mut Vec<ChartContext>,
) -> EngineResult<()> {
let chart_yaml = read_chart_yaml(chart_dir)?;
let is_library = chart_yaml
.chart_type
.as_deref()
.is_some_and(|chart_type| chart_type.eq_ignore_ascii_case("library"));
out.push(ChartContext {
chart_dir: chart_dir.clone(),
values_prefix: parent_prefix.to_vec(),
is_library,
dependency_activation_chain: dependency_activation_chain.to_vec(),
});
let dependency_metadata_by_name = dependency_metadata_map(&chart_yaml, parent_prefix);
let vendor_charts_dir = chart_dir.join("charts")?;
if !vendor_charts_dir.is_dir()? {
return Ok(());
}
let mut vendor_entries: Vec<VfsPath> = vendor_charts_dir.read_dir()?.collect();
vendor_entries.sort_by_key(VfsPath::filename);
for entry in vendor_entries {
let sub_dir = if entry.is_dir()? {
let chart_yaml_path = entry.join("Chart.yaml")?;
let chart_template_yaml_path = entry.join("Chart.template.yaml")?;
if !chart_yaml_path.is_file()? && !chart_template_yaml_path.is_file()? {
continue;
}
entry
} else if entry.is_file()? {
if !is_chart_archive(&entry.filename()) {
continue;
}
extract_chart_archive(&entry, load_budget)?
} else {
continue;
};
let sub_chart_yaml = read_chart_yaml(&sub_dir)?;
let sub_name = sub_chart_yaml
.name
.clone()
.or_else(|| {
let name = sub_dir.filename();
if name.is_empty() { None } else { Some(name) }
})
.ok_or_else(|| CliError::SubchartNameMissing {
path: sub_dir.as_str().to_string(),
})?;
let dependency_metadata = dependency_metadata_by_name
.get(&sub_name)
.cloned()
.unwrap_or_else(|| DependencyMetadata {
values_key: sub_name.clone(),
activation: ChartDependencyActivation::default(),
});
let mut prefix = parent_prefix.to_vec();
prefix.push(dependency_metadata.values_key);
let mut chain = dependency_activation_chain.to_vec();
let activation = dependency_metadata.activation;
if !activation.condition_paths.is_empty() || !activation.tag_paths.is_empty() {
chain.push(activation);
}
discover_chart_contexts_inner(&sub_dir, &prefix, &chain, load_budget, out)?;
}
Ok(())
}
fn is_chart_archive(file_name: &str) -> bool {
let path = Path::new(file_name);
let is_tgz = path
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| extension.eq_ignore_ascii_case("tgz"));
let is_tar_gz = path
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| extension.eq_ignore_ascii_case("gz"))
&& path
.file_stem()
.and_then(|stem| Path::new(stem).extension())
.and_then(|extension| extension.to_str())
.is_some_and(|extension| extension.eq_ignore_ascii_case("tar"));
is_tgz || is_tar_gz
}
fn extract_chart_archive(path: &VfsPath, load_budget: LoadBudget) -> EngineResult<VfsPath> {
let mut file = path.open_file()?;
let bytes = read_to_end_capped(
&mut file,
load_budget.max_chart_archive_bytes,
path.as_str().to_string(),
)?;
let gz = GzDecoder::new(bytes.as_slice());
let mut archive = tar::Archive::new(gz);
let root = VfsPath::new(vfs::MemoryFS::new());
let mut extracted_entries = 0usize;
let mut extracted_bytes = 0usize;
for entry in archive.entries()? {
let mut entry = entry?;
if !entry.header().entry_type().is_file() {
continue;
}
extracted_entries = extracted_entries.saturating_add(1);
if extracted_entries > load_budget.max_chart_archive_entries {
return Err(CliError::LoadEntryBudgetExceeded {
subject: path.as_str().to_string(),
limit_entries: load_budget.max_chart_archive_entries,
});
}
let entry_path = entry.path()?;
validate_archive_entry_path(path.as_str(), &entry_path)?;
let path_str = entry_path.to_string_lossy();
let out = root.join(path_str.as_ref())?;
out.parent().create_dir_all()?;
let mut file = out.create_file()?;
let copied = std::io::copy(&mut entry, &mut file)?;
extracted_bytes =
extracted_bytes.saturating_add(usize::try_from(copied).unwrap_or(usize::MAX));
if extracted_bytes > load_budget.max_chart_archive_unpacked_bytes {
return Err(CliError::LoadBudgetExceeded {
subject: format!("expanded {}", path.as_str()),
limit_bytes: load_budget.max_chart_archive_unpacked_bytes,
});
}
}
find_chart_dir(&root)?.ok_or_else(|| CliError::NoChartYamlInArchive {
archive: path.as_str().to_string(),
})
}
pub(crate) fn validate_archive_entry_path(archive: &str, entry_path: &Path) -> EngineResult<()> {
let is_safe = entry_path.components().all(|component| match component {
Component::Normal(name) => !name.to_string_lossy().contains('\\'),
Component::CurDir => true,
_ => false,
});
if is_safe {
return Ok(());
}
Err(CliError::UnsafeArchiveEntryPath {
archive: archive.to_string(),
entry_path: entry_path.display().to_string(),
})
}
fn find_chart_dir(root: &VfsPath) -> EngineResult<Option<VfsPath>> {
let direct = root.join("Chart.yaml")?;
let direct_template = root.join("Chart.template.yaml")?;
if direct.is_file()? || direct_template.is_file()? {
return Ok(Some(root.clone()));
}
let mut entries: Vec<VfsPath> = root.read_dir()?.collect();
entries.sort_by_key(VfsPath::filename);
for entry in entries {
if !entry.is_dir()? {
continue;
}
let chart_yaml = entry.join("Chart.yaml")?;
let chart_template_yaml = entry.join("Chart.template.yaml")?;
if chart_yaml.is_file()? || chart_template_yaml.is_file()? {
return Ok(Some(entry));
}
}
Ok(None)
}
fn dependency_metadata_map(
chart_yaml: &ChartYaml,
parent_prefix: &[String],
) -> BTreeMap<String, DependencyMetadata> {
let mut out = BTreeMap::new();
let deps = chart_yaml.dependencies.as_deref().unwrap_or_default();
for dependency in deps {
let values_key = dependency
.alias
.clone()
.unwrap_or_else(|| dependency.name.clone());
out.insert(
dependency.name.clone(),
DependencyMetadata {
values_key,
activation: dependency_activation(dependency, parent_prefix),
},
);
}
out
}
fn dependency_activation(
dependency: &ChartDependency,
parent_prefix: &[String],
) -> ChartDependencyActivation {
let condition_paths = dependency
.condition
.as_deref()
.map(|condition| dependency_condition_paths(condition, parent_prefix))
.unwrap_or_default();
let tag_paths = dependency
.tags
.as_deref()
.map(dependency_tag_paths)
.unwrap_or_default();
ChartDependencyActivation {
condition_paths,
tag_paths,
}
}
fn dependency_condition_paths(condition: &str, parent_prefix: &[String]) -> Vec<String> {
let mut seen = BTreeSet::new();
let mut paths = Vec::new();
for path in condition
.split(',')
.map(str::trim)
.filter(|path| !path.is_empty())
.map(|path| scope_chart_yaml_value_path(path, parent_prefix))
{
if seen.insert(path.clone()) {
paths.push(path);
}
}
paths
}
fn dependency_tag_paths(tags: &[String]) -> Vec<String> {
tags.iter()
.map(|tag| tag.trim())
.filter(|tag| !tag.is_empty())
.map(|tag| format!("tags.{tag}"))
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
fn scope_chart_yaml_value_path(path: &str, prefix: &[String]) -> String {
let path = path.trim();
if path == "tags" || path.starts_with("tags.") {
return path.to_string();
}
scope_values_path(path, prefix)
}
fn read_chart_yaml(chart_dir: &VfsPath) -> EngineResult<ChartYaml> {
let chart_yaml = chart_dir.join("Chart.yaml")?;
let chart_template_yaml = chart_dir.join("Chart.template.yaml")?;
let path = if chart_yaml.is_file()? {
chart_yaml
} else if chart_template_yaml.is_file()? {
chart_template_yaml
} else {
chart_yaml
};
Ok(serde_yaml::from_str(&path.read_to_string()?)?)
}