mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
feat(k8s): a Helm chart deploys its objects, images and values
A Chart node had one kind of outgoing edge, DEPENDS_ON for a dependency
its Chart.yaml declares. A chart without dependencies, most of them, was
an isolated leaf: nothing linked it to the objects its templates render,
to its values files, or to the images it runs. On 20 pinned infra
corpora, 238 of 260 Charts had no outgoing edge, while 2,626 objects sat
in chart templates/ directories.
- Chart -DEPLOYS {via: chart}-> each Resource of <chart>/templates/, for
its nearest enclosing chart: a subchart's objects are the subchart's.
The same edge a Flux Kustomization draws to what it applies.
- values.yaml / values-<x>.yaml beside Chart.yaml -CONFIGURES {strategy:
helm_values}-> the Chart.
- Chart -DEPLOYS {via: chart, ref: image}-> each Image its objects name
literally, read after pass_docker.c moved references onto component
Images. A templated image names no Image, and no values default is read.
An incremental closure repair re-runs the K8s pass over its plan only, so
a values file or a template edited alone would lose the chart's edge; such
an edit now declines to the full rebuild, like a Chart.yaml edit.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
ec6427c381
commit
851096ad37
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);
|
||||
|
||||
@@ -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
|
||||
* "<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
|
||||
@@ -6180,6 +6251,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