Break-glass merge PR #2663

This commit is contained in:
Martin Vogel authored and GitHub committed 2026-10-16 18:16:50 +02:00
commit e7ea0aff8e
5 files changed
+288

No files matched your search

+142
View File
@@ -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 <chart>/templates/ -- its nearest enclosing chart, so a
* subchart's objects are the subchart's. Its values files (values.yaml,
* values-<x>.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);
+40
View File
@@ -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";
}
+3
View File
@@ -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-<x>.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);
+72
View File
@@ -4432,6 +4432,77 @@ TEST(probe_helm_templated_objects_are_resources) {
PASS();
}
/* Every DEPLOYS edge out of a Chart and CONFIGURES edge into one, as
* "<source> -> <label>:<target> (<type>[ <ref>])", a File by its path,
* sorted, one per line. */
static char *gpf_chart_edges(const GpfFile *files, int nfiles) {
GpfProj lp;
cbm_store_t *store = gpf_index_files(&lp, files, nfiles);
char *out = NULL;
if (store) {
sqlite3_stmt *st = NULL;
const char *sql =
"SELECT coalesce(group_concat(x, char(10)), '') FROM (SELECT CASE WHEN "
"s.label='File' THEN s.file_path ELSE s.name END || ' -> ' || t.label || ':' || "
"t.name || ' (' || e.type || coalesce(' ' || json_extract(e.properties,'$.ref'), '') "
"|| ')' AS x FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON "
"t.id=e.target_id WHERE e.project=?1 AND ((s.label='Chart' AND e.type='DEPLOYS') OR "
"(t.label='Chart' AND e.type='CONFIGURES')) ORDER BY x)";
if (sqlite3_prepare_v2(cbm_store_get_db(store), sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, lp.project, -1, SQLITE_STATIC);
if (sqlite3_step(st) == SQLITE_ROW) {
out = strdup((const char *)sqlite3_column_text(st, 0));
}
}
sqlite3_finalize(st);
}
gpf_cleanup(&lp, store);
return out;
}
/* A chart deploys what its templates render: Chart -DEPLOYS {via: chart}->
* each object of its templates/ (a subchart's objects are the subchart's),
* and each Image those objects name literally ({ref: image}) -- a templated
* image names none, and no values default is read. Its values files
* (values.yaml, values-*.yaml beside Chart.yaml) CONFIGURE it; a values file
* elsewhere (ci/) does not. */
TEST(probe_helm_chart_deploys_its_objects) {
static const GpfFile files[] = {
{"charts/web/Chart.yaml", "apiVersion: v2\nname: web\nversion: 1.0.0\n"},
{"charts/web/values.yaml", "image:\n repository: nginx\n tag: \"1.27\"\nreplicas: 1\n"},
{"charts/web/values-prod.yaml", "replicas: 3\n"},
{"charts/web/ci/test-values.yaml", "replicas: 2\n"},
{"charts/web/templates/deploy.yaml",
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: web\nspec:\n"
" replicas: {{ .Values.replicas }}\n template:\n spec:\n containers:\n"
" - name: web\n"
" image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"
" - name: side\n image: ghcr.io/acme/sidecar:1\n"},
{"charts/web/templates/svc.yaml",
"apiVersion: v1\nkind: Service\nmetadata:\n name: web\nspec:\n selector:\n"
" app: web\n"},
{"charts/web/charts/db/Chart.yaml", "apiVersion: v2\nname: db\nversion: 0.1.0\n"},
{"charts/web/charts/db/templates/sts.yaml",
"apiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: db\nspec:\n template:\n"
" spec:\n containers:\n - name: db\n"
" image: ghcr.io/acme/db:16\n"}};
char *got = gpf_chart_edges(files, 8);
static const char want[] = "charts/web/values-prod.yaml -> Chart:web (CONFIGURES)\n"
"charts/web/values.yaml -> Chart:web (CONFIGURES)\n"
"db -> Image:ghcr.io/acme/db (DEPLOYS image)\n"
"db -> Resource:StatefulSet/db (DEPLOYS)\n"
"web -> Image:ghcr.io/acme/sidecar (DEPLOYS image)\n"
"web -> Resource:Deployment/web (DEPLOYS)\n"
"web -> Resource:Service/web (DEPLOYS)";
if (!got || strcmp(got, want) != 0) {
fprintf(stderr, "chart edges got:\n%s\nwant:\n%s\n", got ? got : "(null)", want);
free(got);
FAIL("Chart DEPLOYS / CONFIGURES edge set");
}
free(got);
PASS();
}
/* A template's `include "name"` calls the define of that name in its chart
* family -- the chart and its subcharts share one template namespace -- never
* a same-named define of an unrelated chart; a define may also be written in
@@ -6241,6 +6312,7 @@ SUITE(grammar_probe_f) {
RUN_TEST(probe_go_installer_deploys_charts);
RUN_TEST(probe_terraform_helm_release_deploys_its_chart);
RUN_TEST(probe_helm_templated_objects_are_resources);
RUN_TEST(probe_helm_chart_deploys_its_objects);
RUN_TEST(probe_helm_include_resolves_in_the_chart_family);
RUN_TEST(probe_k8s_selector_stays_in_its_directory);
RUN_TEST(probe_k8s_selector_within_kustomize_build);
+31
View File
@@ -4175,6 +4175,36 @@ TEST(incremental_image_split_change_rebuilds) {
PASS();
}
/* A chart's values file CONFIGURES its Chart (pass_k8s.c). The edge is drawn
* by the K8s pass from the chart directory, which a closure repair of the
* values file alone does not see: an edit of it rebuilds, and the edge stays. */
TEST(incremental_helm_values_edit_keeps_chart_edge) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_helmvalues_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "chart/Chart.yaml", "apiVersion: v2\nname: web\nversion: 1.0.0\n");
write_temp_file(tmp, "chart/values.yaml", "replicas: 1\n");
write_temp_file(tmp, "chart/templates/svc.yaml",
"apiVersion: v1\nkind: Service\nmetadata:\n name: web\n");
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/helmvalues.db", tmp);
static const char edge[] =
"SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON "
"t.id=e.target_id WHERE e.type='CONFIGURES' AND s.file_path='chart/values.yaml' AND "
"t.label='Chart'";
int before = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, edge) : -1;
write_temp_file(tmp, "chart/values.yaml", "replicas: 3\n");
int after = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, edge) : -1;
cbm_incremental_route_t route = cbm_pipeline_incremental_test_last_route();
th_rmtree(tmp);
ASSERT_EQ(before, 1);
ASSERT_EQ(after, 1);
ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_FORCED_FULL);
PASS();
}
/* A compose project's .env feeds its compose file (variables in image,
* build): changing it is no closure repair -- the unchanged compose file's
* services would keep the old image. It rebuilds; no stale image remains. */
@@ -23899,6 +23929,7 @@ SUITE(pipeline_semantic_manifest_repro) {
RUN_TEST(incremental_compose_edit_replaces_build_edges);
RUN_TEST(incremental_env_edit_replaces_scoped_envvars);
RUN_TEST(incremental_image_split_change_rebuilds);
RUN_TEST(incremental_helm_values_edit_keeps_chart_edge);
RUN_TEST(incremental_compose_dotenv_change_rebuilds);
RUN_TEST(incremental_dockerfile_edit_keeps_docker_build_edges);
RUN_TEST(incremental_second_build_declarer_keeps_the_edge);