fix(helm): resolve file:// chart dependencies and keep their repository

Chart.yaml dependencies became one external Chart node per name and
nothing else: a `file://../common` dependency -- the library chart in the
next directory -- was an "external" chart, every edge lost where the
dependency comes from, and the parser kept at most 128 entries.

- The parser reads each entry's name and repository (keys in any order;
  lists nested in an entry, such as import-values, name no dependency of
  their own) into a growing array.
- A `file://` dependency that names a chart of this repository links
  DEPENDS_ON to that chart's node, resolved after every Chart.yaml is
  read so the order of the files does not matter.
- Any other dependency -- remote, OCI, or a `file://` path the repository
  cannot resolve (deckhouse names its build container's /deckhouse/...) --
  links to the shared external node as before.
- Every DEPENDS_ON edge carries the entry's repository.

On real input: kubermatic 15/15 and deckhouse 2/2 dependency edges now
carry their repository, prometheus-community/helm-charts 8 of 10 (two
entries name none); Chart nodes and edge counts unchanged. None of the 17
corpora has a relative `file://` dependency; the new pipeline probe covers
it.

Probes: the local-chart link fails with the file:// branch disabled, the
nested-list parser test fails with a parser that takes every dash and key
in the block.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-10-10 13:16:53 +02:00
parent 52ca6b7e19
commit 29864dc0cd
5 changed files with 305 additions and 31 deletions
+64 -10
View File
@@ -1272,14 +1272,51 @@ static const char *helm_match_key(const char *content, const char *eol, const ch
return content + kl + 1;
}
/* Append an empty dependency entry; returns it, or NULL when out of memory. */
static cbm_helm_dep_t *helm_push_dep(cbm_helm_chart_t *out) {
if (out->dep_count == out->dep_cap) {
enum { HELM_DEPS_MIN = 8 };
int cap = out->dep_cap ? out->dep_cap * 2 : HELM_DEPS_MIN;
cbm_helm_dep_t *grown =
cbm_realloc(CBM_MEM_CLASS_OTHER, out->deps, (size_t)cap * sizeof(*grown));
if (!grown) {
return NULL;
}
out->deps = grown;
out->dep_cap = cap;
}
cbm_helm_dep_t *dep = &out->deps[out->dep_count++];
dep->name[0] = '\0';
dep->repository[0] = '\0';
return dep;
}
void cbm_helm_chart_free(cbm_helm_chart_t *hc) {
if (!hc) {
return;
}
cbm_free(CBM_MEM_CLASS_OTHER, hc->deps);
hc->deps = NULL;
hc->dep_count = 0;
hc->dep_cap = 0;
}
int cbm_parse_helm_chart(const char *source, cbm_helm_chart_t *out) {
if (!source || !out) {
if (!out) {
return -1;
}
out->chart_name[0] = '\0';
out->deps = NULL;
out->dep_count = 0;
out->dep_cap = 0;
if (!source) {
return -1;
}
bool in_deps = false;
cbm_helm_dep_t *cur = NULL; /* the dependency entry being read */
int item_indent = -1; /* indent of the entries' "- " */
int key_col = -1; /* column of an entry's keys */
const char *p = source;
while (*p) {
const char *eol = strchr(p, '\n');
@@ -1296,26 +1333,35 @@ int cbm_parse_helm_chart(const char *source, cbm_helm_chart_t *out) {
if (indent == 0) {
/* New top-level key ends any dependencies block. */
in_deps = (helm_match_key(content, eol, "dependencies") != NULL);
cur = NULL;
item_indent = -1;
key_col = -1;
const char *nv = helm_match_key(content, eol, "name");
if (!in_deps && nv && out->chart_name[0] == '\0') {
helm_copy_scalar(nv, eol, out->chart_name, CBM_HELM_NAME_MAX);
}
} else if (in_deps) {
/* List item: optional leading "- ", then look for name:. */
/* A list item ("- ") at the entries' indent opens an entry; its
* keys share the dash line or follow at the same key column.
* Deeper lines (import-values, tags) belong to the entry. */
const char *q = content;
if (*q == '-') {
bool opens = *q == '-' && (item_indent < 0 || indent == item_indent);
if (opens) {
item_indent = indent;
q++;
while (q < eol && *q == ' ') {
q++;
}
key_col = indent + (int)(q - content);
cur = helm_push_dep(out);
}
const char *nv = helm_match_key(q, eol, "name");
if (nv && out->dep_count < CBM_HELM_MAX_DEPS) {
char buf[CBM_HELM_NAME_MAX];
helm_copy_scalar(nv, eol, buf, CBM_HELM_NAME_MAX);
if (buf[0]) {
memcpy(out->deps[out->dep_count], buf, CBM_HELM_NAME_MAX);
out->dep_count++;
if (cur && (opens || indent == key_col)) {
const char *nv = helm_match_key(q, eol, "name");
const char *rv = helm_match_key(q, eol, "repository");
if (nv) {
helm_copy_scalar(nv, eol, cur->name, CBM_HELM_NAME_MAX);
} else if (rv) {
helm_copy_scalar(rv, eol, cur->repository, CBM_HELM_REPO_MAX);
}
}
}
@@ -1325,5 +1371,13 @@ int cbm_parse_helm_chart(const char *source, cbm_helm_chart_t *out) {
}
p = eol + 1;
}
/* Keep only named entries (a dependency without a name names nothing). */
int kept = 0;
for (int i = 0; i < out->dep_count; i++) {
if (out->deps[i].name[0]) {
out->deps[kept++] = out->deps[i];
}
}
out->dep_count = kept;
return (out->chart_name[0] || out->dep_count > 0) ? 0 : -1;
}
+128 -14
View File
@@ -23,6 +23,7 @@
#include "helpers.h" /* cbm_kind_in_set_free_cache */
#include "foundation/slab_alloc.h" /* cbm_slab_destroy_thread */
#include "foundation/platform.h" /* cbm_default_worker_count */
#include "foundation/str_util.h" /* cbm_json_escape */
#include "pipeline/worker_pool.h"
#include <stdatomic.h>
@@ -30,6 +31,8 @@
#include <string.h>
#include <stdio.h>
#define K8S_SLEN(s) (sizeof(s) - 1)
/* ── Internal helpers ────────────────────────────────────────────── */
/* Read entire file into heap-allocated buffer. Returns NULL on error.
@@ -594,17 +597,125 @@ static bool is_helm_chart_file(const char *base) {
return strcmp(base, "Chart.yaml") == 0 || strcmp(base, "Chart.yml") == 0;
}
/* Emit a Chart node for a Chart.yaml and a DEPENDS_ON edge to a (shared,
* deduplicated) Chart node per declared dependency (#338). */
static void handle_helm_chart(cbm_pipeline_ctx_t *ctx, const char *rel_path, const char *source) {
/* A chart's `file://` dependency: the chart in that directory of this
* repository. Linked once every Chart.yaml has been read, so a dependency
* indexed after its dependent still resolves. One that names no chart of this
* repository (an absolute build-container path, a directory without a chart)
* stays a declared dependency on the external node, like a remote one. */
typedef struct {
int64_t chart_id;
char name[CBM_HELM_NAME_MAX]; /* dependency name, for the external fallback */
char *chart_rel; /* the depending Chart.yaml */
char *target_qn; /* QN of the depended-on Chart node; NULL = unresolvable */
char *props; /* edge properties */
} k8s_local_dep_t;
typedef struct {
k8s_local_dep_t *items;
int count;
int cap;
} k8s_local_deps_t;
static bool k8s_is_file_repository(const char *repository) {
return strncmp(repository, "file://", K8S_SLEN("file://")) == 0;
}
static char *k8s_dep_props(const char *repository) {
char escaped[CBM_HELM_REPO_MAX * 2];
cbm_json_escape(escaped, (int)sizeof(escaped), repository);
size_t n = strlen(escaped) + sizeof("{\"repository\":\"\"}");
char *props = cbm_alloc(CBM_MEM_CLASS_OTHER, n);
if (props) {
snprintf(props, n, "{\"repository\":\"%s\"}", escaped);
}
return props;
}
/* DEPENDS_ON from `chart_id` to the shared external Chart node of `name`
* (one per name and project, so charts depending on the same chart meet). */
static bool k8s_external_dep(cbm_pipeline_ctx_t *ctx, int64_t chart_id, const char *name,
const char *chart_rel, const char *props) {
char dep_qn[CBM_SZ_512];
snprintf(dep_qn, sizeof(dep_qn), "%s.__helm_dep__.%s", ctx->project_name, name);
int64_t dep_id = cbm_gbuf_upsert_node(ctx->gbuf, "Chart", name, dep_qn, chart_rel, SKIP_ONE, 0,
"{\"source\":\"helm\",\"external\":true}");
if (dep_id <= 0) {
return false;
}
cbm_gbuf_insert_edge(ctx->gbuf, chart_id, dep_id, "DEPENDS_ON", props ? props : "{}");
return true;
}
static void k8s_local_deps_push(k8s_local_deps_t *deps, int64_t chart_id, const char *chart_rel,
const char *project, const cbm_helm_dep_t *dep) {
const char *dir = dep->repository + K8S_SLEN("file://");
char *target_qn = NULL;
if (dir[0] && dir[0] != '/') { /* an absolute path names the build host, not this repo */
char rel[CBM_HELM_REPO_MAX + K8S_SLEN("./") + K8S_SLEN("/Chart.yaml")];
snprintf(rel, sizeof(rel), "%s%s/Chart.yaml", strncmp(dir, "../", 3) == 0 ? "" : "./", dir);
char *path = cbm_pipeline_resolve_relative_import(chart_rel, rel);
target_qn = path ? cbm_infra_qn(project, path, "helm-chart", NULL) : NULL;
free(path);
}
if (deps->count == deps->cap) {
int cap = deps->cap ? deps->cap * 2 : K8S_GROW_MIN;
k8s_local_dep_t *grown =
cbm_realloc(CBM_MEM_CLASS_OTHER, deps->items, (size_t)cap * sizeof(*grown));
if (!grown) {
free(target_qn);
return;
}
deps->items = grown;
deps->cap = cap;
}
k8s_local_dep_t *d = &deps->items[deps->count++];
*d = (k8s_local_dep_t){.chart_id = chart_id, .target_qn = target_qn};
snprintf(d->name, sizeof(d->name), "%s", dep->name);
d->chart_rel = strdup(chart_rel);
d->props = k8s_dep_props(dep->repository);
}
static void k8s_local_deps_free(k8s_local_deps_t *deps) {
for (int i = 0; i < deps->count; i++) {
free(deps->items[i].target_qn);
free(deps->items[i].chart_rel);
cbm_free(CBM_MEM_CLASS_OTHER, deps->items[i].props);
}
cbm_free(CBM_MEM_CLASS_OTHER, deps->items);
*deps = (k8s_local_deps_t){0};
}
static void k8s_link_local_deps(cbm_pipeline_ctx_t *ctx, k8s_local_deps_t *deps) {
for (int i = 0; i < deps->count; i++) {
const k8s_local_dep_t *d = &deps->items[i];
const cbm_gbuf_node_t *target =
d->target_qn ? cbm_gbuf_find_by_qn(ctx->gbuf, d->target_qn) : NULL;
if (target && target->label && strcmp(target->label, "Chart") == 0 &&
target->id != d->chart_id) {
cbm_gbuf_insert_edge(ctx->gbuf, d->chart_id, target->id, "DEPENDS_ON", d->props);
} else if (d->chart_rel) {
(void)k8s_external_dep(ctx, d->chart_id, d->name, d->chart_rel, d->props);
}
}
k8s_local_deps_free(deps);
}
/* Emit a Chart node for a Chart.yaml and a DEPENDS_ON edge per declared
* dependency (#338): to the chart in this repository for a resolvable
* `file://` dependency (once all charts are read), otherwise to a shared
* external Chart node per dependency name, the repository on the edge. */
static void handle_helm_chart(cbm_pipeline_ctx_t *ctx, const char *rel_path, const char *source,
k8s_local_deps_t *local_deps) {
cbm_helm_chart_t hc;
if (cbm_parse_helm_chart(source, &hc) != 0) {
cbm_helm_chart_free(&hc);
return;
}
const char *cname = hc.chart_name[0] ? hc.chart_name : k8s_basename(rel_path);
char *chart_qn = cbm_infra_qn(ctx->project_name, rel_path, "helm-chart", NULL);
if (!chart_qn) {
cbm_helm_chart_free(&hc);
return;
}
int64_t chart_id = cbm_gbuf_upsert_node(ctx->gbuf, "Chart", cname, chart_qn, rel_path, SKIP_ONE,
@@ -620,18 +731,16 @@ static void handle_helm_chart(cbm_pipeline_ctx_t *ctx, const char *rel_path, con
int dep_edges = 0;
for (int i = 0; i < hc.dep_count && chart_id > 0; i++) {
/* Stable per-project QN so multiple charts depending on the same chart
* link to one shared dependency node. */
char dep_qn[CBM_SZ_512];
snprintf(dep_qn, sizeof(dep_qn), "%s.__helm_dep__.%s", ctx->project_name, hc.deps[i]);
int64_t dep_id =
cbm_gbuf_upsert_node(ctx->gbuf, "Chart", hc.deps[i], dep_qn, rel_path, SKIP_ONE, 0,
"{\"source\":\"helm\",\"external\":true}");
if (dep_id > 0) {
cbm_gbuf_insert_edge(ctx->gbuf, chart_id, dep_id, "DEPENDS_ON", "{}");
dep_edges++;
const cbm_helm_dep_t *dep = &hc.deps[i];
if (k8s_is_file_repository(dep->repository)) {
k8s_local_deps_push(local_deps, chart_id, rel_path, ctx->project_name, dep);
continue;
}
char *props = dep->repository[0] ? k8s_dep_props(dep->repository) : NULL;
dep_edges += k8s_external_dep(ctx, chart_id, dep->name, rel_path, props);
cbm_free(CBM_MEM_CLASS_OTHER, props);
}
cbm_helm_chart_free(&hc);
cbm_log_info("pass.k8s.helm", "file", rel_path, "deps", itoa_k8s(dep_edges));
}
@@ -872,6 +981,7 @@ int cbm_pipeline_pass_k8s(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
/* Collect per-manifest selector/label records for cross-manifest matching. */
k8s_record_array_t recs = {0};
k8s_local_deps_t local_deps = {0};
k8s_prep_t *prep = cbm_calloc(CBM_MEM_CLASS_OTHER, K8S_CHUNK * sizeof(*prep));
if (!prep) {
@@ -884,6 +994,7 @@ int cbm_pipeline_pass_k8s(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
if (cbm_pipeline_check_cancel(ctx)) {
cbm_free(CBM_MEM_CLASS_OTHER, prep);
k8s_records_free(&recs);
k8s_local_deps_free(&local_deps);
return CBM_NOT_FOUND;
}
memset(prep, 0, (size_t)K8S_CHUNK * sizeof(*prep));
@@ -908,7 +1019,7 @@ int cbm_pipeline_pass_k8s(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
is_requirements_file(base) ? "pypi" : "gomod");
break;
case K8S_HELM:
handle_helm_chart(ctx, rel, p->source);
handle_helm_chart(ctx, rel, p->source, &local_deps);
helm_count++;
break;
case K8S_MANIFEST:
@@ -928,6 +1039,7 @@ int cbm_pipeline_pass_k8s(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
if (cancelled) {
cbm_free(CBM_MEM_CLASS_OTHER, prep);
k8s_records_free(&recs);
k8s_local_deps_free(&local_deps);
return CBM_NOT_FOUND;
}
}
@@ -936,6 +1048,8 @@ int cbm_pipeline_pass_k8s(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
/* Connect Services to the workloads their selectors target (INFRA_MAPS). */
k8s_link_selectors(ctx, &recs);
k8s_records_free(&recs);
/* Link `file://` chart dependencies now that every Chart.yaml is read. */
k8s_link_local_deps(ctx, &local_deps);
cbm_log_info("pass.done", "pass", "k8s", "manifests", itoa_k8s(manifest_count));
(void)helm_count;
+12 -5
View File
@@ -995,17 +995,24 @@ int cbm_parse_shell_source(const char *source, cbm_shell_result_t *out);
/* Parse a Terraform file from source text. Returns 0 if parsed, -1 if empty. */
int cbm_parse_terraform_source(const char *source, cbm_terraform_result_t *out);
/* Helm Chart.yaml parse result: chart name + dependency chart names (#338). */
enum { CBM_HELM_MAX_DEPS = 128, CBM_HELM_NAME_MAX = 128 };
/* Helm Chart.yaml parse result: chart name + its dependencies (#338). */
enum { CBM_HELM_NAME_MAX = 128, CBM_HELM_REPO_MAX = 512 };
typedef struct {
char name[CBM_HELM_NAME_MAX];
char repository[CBM_HELM_REPO_MAX]; /* "" when the entry names none */
} cbm_helm_dep_t;
typedef struct {
char chart_name[CBM_HELM_NAME_MAX];
char deps[CBM_HELM_MAX_DEPS][CBM_HELM_NAME_MAX];
cbm_helm_dep_t *deps; /* heap-owned; release with cbm_helm_chart_free */
int dep_count;
int dep_cap;
} cbm_helm_chart_t;
/* Parse a Helm Chart.yaml: top-level `name:` and `dependencies:` list names.
* Returns 0 if parsed (name or deps found), -1 otherwise. */
/* Parse a Helm Chart.yaml: top-level `name:` and every `dependencies:` entry's
* name and repository. Returns 0 if parsed (name or deps found), -1 otherwise;
* either way the caller releases `out` with cbm_helm_chart_free. */
int cbm_parse_helm_chart(const char *source, cbm_helm_chart_t *out);
void cbm_helm_chart_free(cbm_helm_chart_t *hc);
/* Build an infrastructure QN. Caller must free the returned string. */
char *cbm_infra_qn(const char *project_name, const char *rel_path, const char *infra_type,
+50
View File
@@ -138,6 +138,7 @@ typedef struct {
int modules; /* Module label */
int classes; /* Class label */
int resources; /* Resource label (k8s) */
int charts; /* Chart label (helm) */
int imports; /* IMPORTS edges */
int depends_on; /* DEPENDS_ON edges */
int infra_maps; /* INFRA_MAPS edges */
@@ -154,6 +155,7 @@ static GpfMetrics gpf_metrics_files(const GpfFile *files, int nfiles) {
m.modules = gpf_count_label(store, lp.project, "Module");
m.classes = gpf_count_label(store, lp.project, "Class");
m.resources = gpf_count_label(store, lp.project, "Resource");
m.charts = gpf_count_label(store, lp.project, "Chart");
m.imports = cbm_store_count_edges_by_type(store, lp.project, "IMPORTS");
m.depends_on = cbm_store_count_edges_by_type(store, lp.project, "DEPENDS_ON");
m.infra_maps = cbm_store_count_edges_by_type(store, lp.project, "INFRA_MAPS");
@@ -741,6 +743,52 @@ TEST(probe_kustomize_extensionless_file) {
PASS();
}
/* A `file://` dependency is the chart in that directory of this repository,
* not a published one: `app` depends on the local `common`, and no external
* Chart node stands in for it. `common` is read after `app`. */
TEST(probe_helm_file_dependency_links_local_chart) {
static const GpfFile files[] = {
{"charts/app/Chart.yaml",
"apiVersion: v2\n"
"name: app\n"
"version: 1.0.0\n"
"dependencies:\n"
" - name: common\n"
" repository: file://../common\n"
" version: 1.x\n"},
{"charts/common/Chart.yaml",
"apiVersion: v2\n"
"name: common\n"
"type: library\n"
"version: 1.2.0\n"}
};
GpfMetrics m = gpf_metrics_files(files, 2);
ASSERT_TRUE(m.ok);
ASSERT_EQ(m.charts, 2);
ASSERT_EQ(m.depends_on, 1);
PASS();
}
/* A `file://` path this repository cannot resolve (deckhouse names its build
* container's `/deckhouse/...`) still declares a dependency: it stays the
* external Chart node, the repository on its edge, rather than vanishing. */
TEST(probe_helm_unresolved_file_dependency_stays_external) {
static const GpfFile files[] = {
{"modules/policy/Chart.yaml",
"apiVersion: v2\n"
"name: policy\n"
"version: 1.0.0\n"
"dependencies:\n"
" - name: constraint-templates\n"
" repository: \"file:///deckhouse/modules/policy/charts/constraint_templates\"\n"}
};
GpfMetrics m = gpf_metrics_files(files, 1);
ASSERT_TRUE(m.ok);
ASSERT_EQ(m.charts, 2);
ASSERT_EQ(m.depends_on, 1);
PASS();
}
/* A remote base is outside the repository. The import resolver's symbol-name
* guess walked its URL segments and linked kubermatic's gcp addon to the
* `deploy` function of hack/ci/deploy.sh. A kustomize entry is a path,
@@ -1951,6 +1999,8 @@ SUITE(grammar_probe_f) {
RUN_TEST(probe_k8s_selector_after_leading_separator);
RUN_TEST(probe_kustomize_extensionless_file);
RUN_TEST(probe_kustomize_remote_base_names_no_symbol);
RUN_TEST(probe_helm_file_dependency_links_local_chart);
RUN_TEST(probe_helm_unresolved_file_dependency_stays_external);
RUN_TEST(probe_kustomize_nested_sibling_entries);
RUN_TEST(probe_kustomize_single_module);
+51 -2
View File
@@ -14165,8 +14165,11 @@ TEST(helm_parse_chart_dependencies_issue338) {
ASSERT_EQ(cbm_parse_helm_chart(src, &hc), 0);
ASSERT_STR_EQ(hc.chart_name, "mychart");
ASSERT_EQ(hc.dep_count, 2);
ASSERT_STR_EQ(hc.deps[0], "postgresql");
ASSERT_STR_EQ(hc.deps[1], "redis");
ASSERT_STR_EQ(hc.deps[0].name, "postgresql");
ASSERT_STR_EQ(hc.deps[0].repository, "https://charts.bitnami.com/bitnami");
ASSERT_STR_EQ(hc.deps[1].name, "redis");
ASSERT_STR_EQ(hc.deps[1].repository, "https://charts.bitnami.com/bitnami");
cbm_helm_chart_free(&hc);
PASS();
}
@@ -14175,6 +14178,50 @@ TEST(helm_parse_chart_no_deps_issue338) {
ASSERT_EQ(cbm_parse_helm_chart("name: solo\nversion: 0.1.0\n", &hc), 0);
ASSERT_STR_EQ(hc.chart_name, "solo");
ASSERT_EQ(hc.dep_count, 0);
cbm_helm_chart_free(&hc);
PASS();
}
/* An entry's keys come in any order; lists nested in an entry
* (import-values, tags) belong to it and name no dependency of their own. */
TEST(helm_parse_chart_dependency_entries) {
const char *src = "name: app\n"
"dependencies:\n"
" - repository: file://../common\n"
" name: common\n"
" import-values:\n"
" - child: default.data\n"
" parent: imported\n"
" - name: not-a-dependency\n"
" - name: redis\n"
" tags:\n"
" - cache\n";
cbm_helm_chart_t hc;
ASSERT_EQ(cbm_parse_helm_chart(src, &hc), 0);
ASSERT_EQ(hc.dep_count, 2);
ASSERT_STR_EQ(hc.deps[0].name, "common");
ASSERT_STR_EQ(hc.deps[0].repository, "file://../common");
ASSERT_STR_EQ(hc.deps[1].name, "redis");
ASSERT_STR_EQ(hc.deps[1].repository, "");
cbm_helm_chart_free(&hc);
PASS();
}
/* No dependency count decides what is read: the parser kept 128. */
TEST(helm_parse_chart_many_dependencies) {
enum { DEPS = 130 };
char *src = malloc(DEPS * 32 + 64);
ASSERT_NOT_NULL(src);
int n = snprintf(src, 64, "name: umbrella\ndependencies:\n");
for (int i = 0; i < DEPS; i++) {
n += snprintf(src + n, 32, " - name: dep-%03d\n", i);
}
cbm_helm_chart_t hc;
ASSERT_EQ(cbm_parse_helm_chart(src, &hc), 0);
free(src);
ASSERT_EQ(hc.dep_count, DEPS);
ASSERT_STR_EQ(hc.deps[DEPS - 1].name, "dep-129");
cbm_helm_chart_free(&hc);
PASS();
}
@@ -19702,6 +19749,8 @@ SUITE(pipeline) {
RUN_TEST(infra_parse_terraform_empty);
RUN_TEST(helm_parse_chart_dependencies_issue338);
RUN_TEST(helm_parse_chart_no_deps_issue338);
RUN_TEST(helm_parse_chart_dependency_entries);
RUN_TEST(helm_parse_chart_many_dependencies);
/* Infrascan: QN helper */
RUN_TEST(infra_qn_helper);
/* Infrascan: pipeline integration */