From 29864dc0cdff7a961b2bb1f5c17c419bfba4f573 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sat, 10 Oct 2026 13:16:53 +0200 Subject: [PATCH] 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 --- src/pipeline/pass_infrascan.c | 74 +++++++++++++--- src/pipeline/pass_k8s.c | 142 ++++++++++++++++++++++++++++--- src/pipeline/pipeline_internal.h | 17 ++-- tests/test_grammar_probe_f.c | 50 +++++++++++ tests/test_pipeline.c | 53 +++++++++++- 5 files changed, 305 insertions(+), 31 deletions(-) diff --git a/src/pipeline/pass_infrascan.c b/src/pipeline/pass_infrascan.c index a9b2418fd..24af73d86 100644 --- a/src/pipeline/pass_infrascan.c +++ b/src/pipeline/pass_infrascan.c @@ -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; } diff --git a/src/pipeline/pass_k8s.c b/src/pipeline/pass_k8s.c index 047df4355..34a6aeb76 100644 --- a/src/pipeline/pass_k8s.c +++ b/src/pipeline/pass_k8s.c @@ -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 @@ -30,6 +31,8 @@ #include #include +#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; diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index 87b9c160b..544bc3412 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -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, diff --git a/tests/test_grammar_probe_f.c b/tests/test_grammar_probe_f.c index bdfc9c4b5..ec569c312 100644 --- a/tests/test_grammar_probe_f.c +++ b/tests/test_grammar_probe_f.c @@ -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); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 68166f0c4..5ac761369 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -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 */