mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Break-glass merge PR #2663
This commit is contained in:
5 files changed
+288
No files matched your search
@@ -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);
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user