diff --git a/src/pipeline/pass_k8s.c b/src/pipeline/pass_k8s.c index 1af48be43d..a9e92acc76 100644 --- a/src/pipeline/pass_k8s.c +++ b/src/pipeline/pass_k8s.c @@ -3631,6 +3631,145 @@ static int64_t k8s_external_chart(k8s_dep_link_t *l, const char *name, const cha return cbm_pipeline_external_chart(l->ctx, name, url, file); } +/* ── A chart's own edges (PHASE 2 P2.4) ────────────────────────────── */ + +bool cbm_helm_values_name(const char *base) { + size_t vl = K8S_SLEN("values"); + if (!base || strncmp(base, "values", vl) != 0) { + return false; + } + const char *r = base + vl; + if (*r == '-') { + const char *dot = strrchr(r, '.'); + if (!dot || dot == r + SKIP_ONE) { + return false; + } + r = dot; + } + return strcmp(r, ".yaml") == 0 || strcmp(r, ".yml") == 0; +} + +/* Is `rel` a values file directly in chart dir `dir`? */ +static bool k8s_chart_values_file(const char *rel, const char *dir) { + size_t dl = strlen(dir); + if (dl && (strncmp(rel, dir, dl) != 0 || rel[dl] != '/')) { + return false; + } + const char *base = rel + (dl ? dl + SKIP_ONE : 0); + return !strchr(base, '/') && cbm_helm_values_name(base); +} + +/* A chart deploys what its templates render: Chart -DEPLOYS {via: chart}-> + * each Resource of /templates/ -- its nearest enclosing chart, so a + * subchart's objects are the subchart's. Its values files (values.yaml, + * values-.yaml beside Chart.yaml) CONFIGURE {strategy: helm_values} it. */ +static void k8s_link_chart_contents(cbm_pipeline_ctx_t *ctx, k8s_bundles_t *b) { + if (!b->charts || cbm_ht_count(b->charts) == 0) { + return; + } + k8s_dep_link_t l = {.ctx = ctx, .charts = k8s_index_by_file(ctx, "Chart")}; + if (!l.charts) { + return; + } + int configures = 0; + static const char *const labels[] = {"Resource", "File"}; + for (size_t k = 0; k < sizeof(labels) / sizeof(labels[0]); k++) { + const cbm_gbuf_node_t **nodes = NULL; + int n = 0; + if (cbm_gbuf_find_by_label(ctx->gbuf, labels[k], &nodes, &n) != 0) { + continue; + } + for (int i = 0; i < n; i++) { + const char *fp = nodes[i]->file_path; + char dir[CBM_SZ_1K]; + int64_t chart = 0; + if (fp && k8s_chart_of(b, fp, dir, sizeof(dir))) { + chart = k8s_chart_node(&l, dir); + } + if (chart <= 0) { + continue; + } + if (k == 0) { + char tpl[CBM_SZ_1K]; + snprintf(tpl, sizeof(tpl), "%s%stemplates/", dir, dir[0] ? "/" : ""); + if (strncmp(fp, tpl, strlen(tpl)) == 0) { + k8s_deploys_edge(&l, chart, nodes[i]->id, "{\"via\":\"chart\"}"); + } + } else if (k8s_chart_values_file(fp, dir)) { + cbm_gbuf_insert_edge(ctx->gbuf, nodes[i]->id, chart, "CONFIGURES", + "{\"strategy\":\"helm_values\"}"); + configures++; + } + } + } + k8s_free_index(l.charts); + if (l.edges + configures > 0) { + cbm_log_info("pass.k8s.chart_contents", "deploys", itoa_k8s(l.edges), "values", + itoa_k8s(configures)); + } +} + +/* Each Image a chart's objects name literally (their USAGE {ref: image}, after + * pass_docker.c moved references onto component Images): Chart -DEPLOYS + * {via: chart, ref: image}-> it. A templated image names no Image, and no + * values default is read. Edges are collected first: inserting while walking + * a chart's own DEPLOYS edges would grow the list being read. */ +static void k8s_link_chart_images(cbm_pipeline_ctx_t *ctx) { + const cbm_gbuf_node_t **charts = NULL; + int nc = 0; + if (cbm_gbuf_find_by_label(ctx->gbuf, "Chart", &charts, &nc) != 0 || nc == 0) { + return; + } + int64_t *pairs = NULL; + int np = 0; + int cap = 0; + for (int c = 0; c < nc; c++) { + const cbm_gbuf_edge_t **dep = NULL; + int nd = 0; + if (cbm_gbuf_find_edges_by_source_type(ctx->gbuf, charts[c]->id, "DEPLOYS", &dep, &nd) != + 0) { + continue; + } + for (int d = 0; d < nd; d++) { + const char *dp = dep[d]->properties_json; + if (!dp || !strstr(dp, "\"via\":\"chart\"") || strstr(dp, "\"ref\":")) { + continue; + } + const cbm_gbuf_edge_t **use = NULL; + int nu = 0; + if (cbm_gbuf_find_edges_by_source_type(ctx->gbuf, dep[d]->target_id, "USAGE", &use, + &nu) != 0) { + continue; + } + for (int u = 0; u < nu; u++) { + const cbm_gbuf_node_t *img = cbm_gbuf_find_by_id(ctx->gbuf, use[u]->target_id); + const char *up = use[u]->properties_json; + if (!img || !img->label || strcmp(img->label, "Image") != 0 || !up || + !strstr(up, "\"ref\":\"image\"")) { + continue; + } + if (np + PAIR_LEN > cap) { + int ncap = cap ? cap * PAIR_LEN : CBM_SZ_64; + int64_t *grown = + cbm_realloc(CBM_MEM_CLASS_OTHER, pairs, (size_t)ncap * sizeof(*pairs)); + if (!grown) { + break; + } + pairs = grown; + cap = ncap; + } + pairs[np++] = charts[c]->id; + pairs[np++] = img->id; + } + } + } + for (int i = 0; i + SKIP_ONE < np; i += PAIR_LEN) { + cbm_gbuf_insert_edge(ctx->gbuf, pairs[i], pairs[i + SKIP_ONE], "DEPLOYS", + "{\"via\":\"chart\",\"ref\":\"image\"}"); + } + cbm_free(CBM_MEM_CLASS_OTHER, pairs); +} + /* A HelmRelease whose chartRef names OCIRepository `oci` (its USAGE edge * {"ref":"chartRef"}, resolved with the references) deploys the chart that * repository fetches: the external Chart of that OCI reference (its last @@ -4517,6 +4656,7 @@ int cbm_pipeline_pass_k8s(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, * then the templates' includes, which see those dependencies' defines. */ k8s_link_local_deps(ctx, &local_deps); k8s_link_includes(ctx, &bundles, files, file_count); + k8s_link_chart_contents(ctx, &bundles); k8s_bundles_free(&bundles); k8s_kusts_free(&kusts); /* Shell and Make deploy commands, against the nodes linked above. */ @@ -4524,6 +4664,8 @@ int cbm_pipeline_pass_k8s(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, cbm_pipeline_pass_terraform(ctx, files, file_count); cbm_pipeline_pass_ansible(ctx, files, file_count); cbm_pipeline_pass_docker(ctx, files, file_count); + /* The Images a chart's objects use, once the image hubs moved them. */ + k8s_link_chart_images(ctx); /* The variables a container sets, to the readers in its own image. */ (void)cbm_pipeline_env_join(ctx); diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index 519dac7499..99df053e41 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -2456,6 +2456,32 @@ static bool closure_dir_has_compose(const char *repo_path, const char *rel, cons return found; } +/* The nearest directory at or above `rel`'s that holds a Chart.yaml, into out + * ("" for the repository root); false when none does. */ +static bool closure_chart_dir_of(const char *repo_path, const char *rel, char *out, size_t sz) { + snprintf(out, sz, "%s", rel); + char *cut = strrchr(out, '/'); + for (;;) { + if (cut) { + *cut = '\0'; + } else { + out[0] = '\0'; + } + static const char *const names[] = {"Chart.yaml", "Chart.yml"}; + for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i++) { + char path[CBM_SZ_4K]; + snprintf(path, sizeof(path), "%s/%s%s%s", repo_path, out, out[0] ? "/" : "", names[i]); + if (cbm_file_exists(path)) { + return true; + } + } + if (!out[0]) { + return false; + } + cut = strrchr(out, '/'); + } +} + /* Kubernetes inputs reach past their own file (pass_k8s.c): a kustomization * decides which manifests deploy together and in which namespace, a * Chart.yaml links other charts, and a manifest's Services select workloads @@ -2475,6 +2501,20 @@ static const char *closure_k8s_decline(cbm_store_t *store, const char *project, if (strcmp(base, "Chart.yaml") == 0 || strcmp(base, "Chart.yml") == 0) { return "helm_chart_changed"; } + /* A chart's values file CONFIGURES it and its templates/ are what it + * DEPLOYS: edges the K8s pass draws from the chart directory, which a + * repair of this file alone does not hold. */ + char chart[CBM_SZ_4K]; + if (closure_chart_dir_of(repo_path, rel, chart, sizeof(chart))) { + size_t cl = strlen(chart); + const char *in_chart = rel + (cl ? cl + SKIP_ONE : 0); + if (cbm_helm_values_name(in_chart)) { + return "helm_values_changed"; + } + if (strncmp(in_chart, "templates/", strlen("templates/")) == 0) { + return "helm_template_changed"; + } + } if (strcmp(base, ".env") == 0 && closure_dir_has_compose(repo_path, rel, base)) { return "compose_env_changed"; } diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index a54dea245a..b4f9c08c96 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -1057,6 +1057,9 @@ int cbm_parse_compile_commands(const char *json_data, const char *repo_path, cha /* File identification helpers */ bool cbm_is_kustomize_file(const char *name); bool cbm_is_k8s_manifest(const char *name, const char *content); +/* A Helm values file name: values.yaml / .yml, values-.yaml / .yml + * (pass_k8s.c; beside Chart.yaml it CONFIGURES the chart). */ +bool cbm_helm_values_name(const char *base); /* Secret detection */ bool cbm_is_secret_binding(const char *key, const char *value); diff --git a/tests/test_grammar_probe_f.c b/tests/test_grammar_probe_f.c index 6450ab5b5c..a917939fab 100644 --- a/tests/test_grammar_probe_f.c +++ b/tests/test_grammar_probe_f.c @@ -4372,6 +4372,77 @@ TEST(probe_helm_templated_objects_are_resources) { PASS(); } +/* Every DEPLOYS edge out of a Chart and CONFIGURES edge into one, as + * " ->