use color_eyre::eyre;
use indoc::indoc;
use serde::Deserialize;
#[derive(Deserialize)]
struct ChartLock {
#[serde(default)]
dependencies: Vec<LockedDependency>,
}
#[derive(Deserialize)]
struct LockedDependency {
name: String,
version: String,
}
#[derive(Deserialize)]
struct ChartManifest {
#[serde(default)]
dependencies: Vec<ManifestDependency>,
}
#[derive(Deserialize)]
struct ManifestDependency {
name: String,
#[serde(default)]
alias: Option<String>,
}
#[derive(Deserialize)]
struct VendoredChartVersion {
version: String,
}
fn chart_lock_path(chart_dir: &std::path::Path) -> Option<std::path::PathBuf> {
["Chart.lock", "requirements.lock"]
.iter()
.map(|name| chart_dir.join(name))
.find(|path| path.is_file())
}
fn dependency_is_vendored(chart_dir: &std::path::Path, names: &[&str], version: &str) -> bool {
let vendored_charts_dir = chart_dir.join("charts");
names.iter().any(|name| {
if vendored_charts_dir
.join(format!("{name}-{version}.tgz"))
.is_file()
{
return true;
}
let manifest_path = vendored_charts_dir.join(name).join("Chart.yaml");
let Ok(manifest_text) = std::fs::read_to_string(&manifest_path) else {
return false;
};
serde_yaml::from_str::<VendoredChartVersion>(&manifest_text)
.is_ok_and(|manifest| manifest.version == version)
})
}
fn locked_chart_roots(corpus_root: &std::path::Path) -> eyre::Result<Vec<std::path::PathBuf>> {
let mut roots = Vec::new();
let mut stack: Vec<std::path::PathBuf> = std::fs::read_dir(corpus_root)?
.map(|entry| entry.map(|entry| entry.path()))
.collect::<Result<_, _>>()?;
while let Some(chart_dir) = stack.pop() {
if !chart_dir.is_dir() {
continue;
}
let charts_dir = chart_dir.join("charts");
if charts_dir.is_dir() {
for entry in std::fs::read_dir(&charts_dir)? {
stack.push(entry?.path());
}
}
roots.push(chart_dir);
}
roots.sort();
Ok(roots)
}
fn missing_locked_dependencies(
chart_dir: &std::path::Path,
corpus_root: &std::path::Path,
) -> eyre::Result<Vec<String>> {
let Some(lock_path) = chart_lock_path(chart_dir) else {
return Ok(Vec::new());
};
let mut missing = Vec::new();
let lock: ChartLock = serde_yaml::from_str(&std::fs::read_to_string(&lock_path)?)?;
let manifest: ChartManifest =
serde_yaml::from_str(&std::fs::read_to_string(chart_dir.join("Chart.yaml"))?)?;
for dependency in &lock.dependencies {
let mut names = vec![dependency.name.as_str()];
names.extend(manifest.dependencies.iter().filter_map(|declared| {
(declared.name == dependency.name)
.then_some(declared.alias.as_deref())
.flatten()
}));
if !dependency_is_vendored(chart_dir, &names, &dependency.version) {
missing.push(format!(
"{chart}: locked dependency {name} {version} is not vendored under charts/",
chart = chart_dir
.strip_prefix(corpus_root)
.unwrap_or(chart_dir)
.display(),
name = dependency.name,
version = dependency.version,
));
}
}
Ok(missing)
}
#[test]
fn corpus_charts_vendor_every_locked_dependency() -> eyre::Result<()> {
let corpus_root = test_util::workspace_testdata().join("charts");
let mut missing = Vec::new();
for chart_dir in locked_chart_roots(&corpus_root)? {
missing.extend(missing_locked_dependencies(&chart_dir, &corpus_root)?);
}
assert!(
missing.is_empty(),
"missing vendored dependencies:\n{}",
missing.join("\n")
);
Ok(())
}
#[test]
fn nested_dependency_locks_are_discovered() -> eyre::Result<()> {
let corpus_root = test_util::workspace_testdata().join("charts");
let roots = locked_chart_roots(&corpus_root)?;
let nested = corpus_root
.join("signoz-signoz")
.join("charts")
.join("clickhouse");
assert!(
roots.contains(&nested),
"nested chart root {} not discovered",
nested.display()
);
assert!(
chart_lock_path(&nested).is_some(),
"expected the nested clickhouse chart to carry its own lock"
);
Ok(())
}
#[test]
fn unpacked_dependency_with_wrong_version_is_not_vendored() -> eyre::Result<()> {
let chart_dir = tempfile::tempdir()?;
let dependency_dir = chart_dir.path().join("charts").join("common");
std::fs::create_dir_all(&dependency_dir)?;
std::fs::write(
dependency_dir.join("Chart.yaml"),
indoc! {"
name: common
version: 1.0.0
"},
)?;
assert!(dependency_is_vendored(
chart_dir.path(),
&["common"],
"1.0.0"
));
assert!(!dependency_is_vendored(
chart_dir.path(),
&["common"],
"2.0.0"
));
Ok(())
}
#[test]
fn legacy_requirements_lock_is_discovered() {
let datadog = test_util::workspace_testdata()
.join("charts")
.join("datadog");
let lock_path = chart_lock_path(&datadog).expect("datadog lock file discovered");
assert!(lock_path.ends_with("requirements.lock"));
}
#[test]
fn provider_bundle_holds_kubernetes_schemas() -> eyre::Result<()> {
let bundle = test_util::workspace_testdata().join("provider-bundle");
let mut schema_files = 0usize;
let mut stack = vec![bundle.join("kubernetes-json-schema-cache")];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir)? {
let path = entry?.path();
if path.is_dir() {
stack.push(path);
} else if path.extension().is_some_and(|ext| ext == "json") {
schema_files += 1;
}
}
}
assert!(
schema_files > 0,
"the vendored Kubernetes schema bundle at {} holds no schema files; \
corpus fixtures would silently lose their provider-backed facts",
bundle.display()
);
Ok(())
}