feat(terraform): a helm_release DEPLOYS its chart

A Terraform module installing a chart with `resource "helm_release"` had no
edge to the chart: "which modules install this chart" had no answer.

A new Terraform / OpenTofu pass (run at the end of the K8s pass, like the
shell pass) reads the helm_release blocks of .tf / .tofu files and links,
to existing nodes only, from the block's node (resource.helm_release.<name>):
- chart = "./charts/x" or "${path.module}/charts/x": the Chart of that
  directory of the module;
- a repository and a chart name, or chart = "oci://.../<name>": the shared
  external Chart of that name (the node chart dependencies use).
Values are read from string literals and ${path.module} only; a variable
chart, or a directory without a Chart.yaml, is no edge. Properties: via
terraform, the chart, external, line.

Measured on 17 repositories against an independent reading of the
Terraform files: 21 of 21 releases linked, 21 of 21 correct (EKS
blueprints 17, k8s.io 3, kube-hetzner 1). Only these DEPLOYS edges and 8
external Charts were added; three runs build identical graphs; index time
unchanged (cloud-foundation-fabric 1.91 -> 1.89 s, EKS blueprints 0.87 ->
0.88 s, k8s.io 3.44 -> 3.45 s, medians); editing or deleting the files
leaves the graph a fresh index builds.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-11 05:27:23 +02:00
1 parent ae93174b70
commit a197d0c684
6 files changed
+485 -1

No files matched your search

+1
View File
@@ -520,6 +520,7 @@ PIPELINE_SRCS = \
src/pipeline/pass_k8s.c \
src/pipeline/pass_operator.c \
src/pipeline/pass_shell.c \
src/pipeline/pass_terraform.c \
src/pipeline/pass_similarity.c \
src/pipeline/pass_semantic_edges.c \
src/pipeline/pass_complexity.c \
+1 -1
View File
@@ -82,7 +82,7 @@ REFS=(
# Infrastructure hybrid LSP sources outside internal/cbm/lsp/ (scanned too).
INFRA_SRC=(internal/cbm/extract_k8s.c src/pipeline/pass_k8s.c src/pipeline/pass_operator.c
src/pipeline/pass_shell.c)
src/pipeline/pass_shell.c src/pipeline/pass_terraform.c)
ONLY_LANG=""; REFRESH=0; LIST_ONLY=0
while [ $# -gt 0 ]; do
+1
View File
@@ -3414,6 +3414,7 @@ int cbm_pipeline_pass_k8s(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
k8s_kusts_free(&kusts);
/* Shell and Make deploy commands, against the nodes linked above. */
cbm_pipeline_pass_shell(ctx, files, file_count);
cbm_pipeline_pass_terraform(ctx, files, file_count);
cbm_log_info("pass.done", "pass", "k8s", "manifests", itoa_k8s(manifest_count));
(void)helm_count;
+441
View File
@@ -0,0 +1,441 @@
/*
* pass_terraform.c — the Terraform / OpenTofu infrastructure layer.
*
* PHASE 3 P3.4: a `resource "helm_release"` DEPLOYS its chart -- an edge from
* the resource block (its node `resource.helm_release.<name>`) to existing
* nodes only:
* chart = "./charts/x" / "${path.module}/charts/x" the Chart of that
* directory, relative to
* the module's directory
* repository = "<url>", chart = "<name>" the shared external
* chart = "oci://registry/.../<name>" Chart of that name
* Values are evaluated from string literals and ${path.module}; any other
* expression is unknown, and an unknown chart is no edge. Run at the end of
* the K8s pass, over the same files.
*/
#include "foundation/constants.h"
#include "foundation/compat.h"
#include "foundation/compat_fs.h"
#include "foundation/hash_table.h"
#include "foundation/limits.h"
#include "foundation/log.h"
#include "foundation/mem_core.h"
#include "foundation/str_util.h"
#include "graph_buffer/graph_buffer.h"
#include "pipeline/pipeline.h"
#include "pipeline/pipeline_internal.h"
#include "cbm.h"
#include "lang_specs.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
cbm_pipeline_ctx_t *ctx;
CBMHashTable *charts; /* Chart.yaml path -> local Chart node id */
const char *src;
const char *file;
char mdir[CBM_SZ_1K]; /* the module's directory */
int edges;
} tf_t;
static bool tf_type(TSNode n, const char *t) {
return !ts_node_is_null(n) && strcmp(ts_node_type(n), t) == 0;
}
static const char *tf_basename(const char *p) {
const char *s = strrchr(p, '/');
return s ? s + SKIP_ONE : p;
}
/* Append [p, p+n) to out (size sz); false when it does not fit. */
static bool tf_add(char *out, size_t sz, size_t *len, const char *p, size_t n) {
if (*len + n + SKIP_ONE > sz) {
return false;
}
memcpy(out + *len, p, n);
*len += n;
out[*len] = '\0';
return true;
}
/* A string value: a literal, or a template of literals and ${path.module}.
* false when any part is something else. */
static bool tf_string(const tf_t *t, TSNode n, char *out, size_t sz) {
size_t len = 0;
out[0] = '\0';
while (tf_type(n, "expression") || tf_type(n, "literal_value") || tf_type(n, "template_expr") ||
tf_type(n, "string_lit") || tf_type(n, "quoted_template")) {
if (tf_type(n, "string_lit") || tf_type(n, "quoted_template")) {
break;
}
if (ts_node_named_child_count(n) != SKIP_ONE) {
return false;
}
n = ts_node_named_child(n, 0);
}
if (!tf_type(n, "string_lit") && !tf_type(n, "quoted_template")) {
return false;
}
for (uint32_t i = 0; i < ts_node_named_child_count(n); i++) {
TSNode c = ts_node_named_child(n, i);
uint32_t b = ts_node_start_byte(c);
uint32_t e = ts_node_end_byte(c);
if (tf_type(c, "quoted_template_start") || tf_type(c, "quoted_template_end")) {
continue;
}
if (tf_type(c, "template_literal")) {
if (!tf_add(out, sz, &len, t->src + b, e - b)) {
return false;
}
continue;
}
if (!tf_type(c, "template_interpolation")) {
return false;
}
/* ${path.module} only */
TSNode expr = {0};
for (uint32_t j = 0; j < ts_node_named_child_count(c); j++) {
if (tf_type(ts_node_named_child(c, j), "expression")) {
expr = ts_node_named_child(c, j);
}
}
if (ts_node_is_null(expr)) {
return false;
}
uint32_t xb = ts_node_start_byte(expr);
uint32_t xe = ts_node_end_byte(expr);
static const char PATH_MODULE[] = "path.module";
if (xe - xb != sizeof(PATH_MODULE) - SKIP_ONE ||
strncmp(t->src + xb, PATH_MODULE, xe - xb) != 0) {
return false;
}
/* an absolute marker: the module directory, from the repo root */
if (!tf_add(out, sz, &len, "\x01", SKIP_ONE) ||
!tf_add(out, sz, &len, t->mdir, strlen(t->mdir))) {
return false;
}
}
return true;
}
/* A path inside the module directory: "./a/../b" -> "<mdir>/b"; NULL past
* the repository root. A value starting with the \x01 marker is already
* from the root. */
static bool tf_repo_path(const char *mdir, const char *v, char *out, size_t sz) {
bool anchored = v[0] == '\x01';
const char *rel = anchored ? v + SKIP_ONE : v;
char buf[CBM_SZ_2K];
if (anchored) {
snprintf(buf, sizeof(buf), "%s", rel);
} else {
snprintf(buf, sizeof(buf), "%s%s%s", mdir, mdir[0] ? "/" : "", rel);
}
size_t n = 0;
out[0] = '\0';
for (const char *p = buf; *p;) {
while (*p == '/') {
p++;
}
size_t sl = strcspn(p, "/");
if (sl == 0) {
break;
}
if (sl == SKIP_ONE && p[0] == '.') {
p += sl;
continue;
}
if (sl == PAIR_LEN && p[0] == '.' && p[SKIP_ONE] == '.') {
if (n == 0) {
return false;
}
while (n > 0 && out[n - SKIP_ONE] != '/') {
n--;
}
if (n > 0) {
n--;
}
out[n] = '\0';
p += sl;
continue;
}
if (n + sl + PAIR_LEN > sz) {
return false;
}
if (n) {
out[n++] = '/';
}
memcpy(out + n, p, sl);
n += sl;
out[n] = '\0';
p += sl;
}
return true;
}
/* Chart.yaml path -> its (local) Chart node. */
static CBMHashTable *tf_chart_index(cbm_pipeline_ctx_t *ctx) {
CBMHashTable *ix = cbm_ht_create(0);
const cbm_gbuf_node_t **nodes = NULL;
int count = 0;
if (!ix || cbm_gbuf_find_by_label(ctx->gbuf, "Chart", &nodes, &count) != 0) {
return ix;
}
for (int i = 0; i < count; i++) {
const char *fp = nodes[i]->file_path;
const char *qn = nodes[i]->qualified_name;
const char *base = fp ? tf_basename(fp) : "";
if (fp && qn && !strstr(qn, ".__helm_dep__.") &&
(strcmp(base, "Chart.yaml") == 0 || strcmp(base, "Chart.yml") == 0)) {
cbm_ht_set(ix, fp, (void *)(intptr_t)nodes[i]->id);
}
}
return ix;
}
/* The Chart node of repository chart directory `dir`, 0 when none. */
static int64_t tf_local_chart(const tf_t *t, const char *dir) {
static const char *const names[] = {"Chart.yaml", "Chart.yml"};
for (size_t k = 0; t->charts && k < sizeof(names) / sizeof(names[0]); k++) {
char want[CBM_SZ_2K];
snprintf(want, sizeof(want), "%s%s%s", dir, dir[0] ? "/" : "", names[k]);
int64_t id = (int64_t)(intptr_t)cbm_ht_get(t->charts, want);
if (id > 0) {
return id;
}
}
return 0;
}
/* The shared external Chart of `name` (the one chart dependencies use). */
static int64_t tf_external_chart(cbm_pipeline_ctx_t *ctx, const char *name, const char *file) {
char qn[CBM_SZ_512];
snprintf(qn, sizeof(qn), "%s.__helm_dep__.%s", ctx->project_name, name);
const cbm_gbuf_node_t *known = cbm_gbuf_find_by_qn(ctx->gbuf, qn);
if (known) {
return known->id;
}
return cbm_gbuf_upsert_node(ctx->gbuf, "Chart", name, qn, file, SKIP_ONE, 0,
"{\"source\":\"helm\",\"external\":true}");
}
/* The node of block `resource.helm_release.<name>` in the file. */
static int64_t tf_block_node(const tf_t *t, const char *name) {
char full[CBM_SZ_512];
snprintf(full, sizeof(full), "resource.helm_release.%s", name);
const cbm_gbuf_node_t **nodes = NULL;
int count = 0;
if (cbm_gbuf_find_by_name(t->ctx->gbuf, full, &nodes, &count) != 0) {
return 0;
}
for (int i = 0; i < count; i++) {
if (nodes[i]->file_path && strcmp(nodes[i]->file_path, t->file) == 0) {
return nodes[i]->id;
}
}
return 0;
}
static bool tf_plain_name(const char *s) {
if (!s[0]) {
return false;
}
for (const char *p = s; *p; p++) {
if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') ||
*p == '-' || *p == '_' || *p == '.')) {
return false;
}
}
return true;
}
/* One `resource "helm_release" "<name>" { ... }` block. */
static void tf_helm_release(tf_t *t, TSNode block, const char *name) {
TSNode body = {0};
for (uint32_t i = 0; i < ts_node_named_child_count(block); i++) {
if (tf_type(ts_node_named_child(block, i), "body")) {
body = ts_node_named_child(block, i);
}
}
if (ts_node_is_null(body)) {
return;
}
char chart[CBM_SZ_1K] = "";
bool has_chart = false;
bool has_repo = false; /* a repository, even of unknown value: not a local chart */
for (uint32_t i = 0; i < ts_node_named_child_count(body); i++) {
TSNode a = ts_node_named_child(body, i);
if (!tf_type(a, "attribute") || ts_node_named_child_count(a) < PAIR_LEN) {
continue;
}
TSNode key = ts_node_named_child(a, 0);
TSNode val = ts_node_named_child(a, SKIP_ONE);
uint32_t kb = ts_node_start_byte(key);
uint32_t ke = ts_node_end_byte(key);
size_t kl = ke - kb;
if (kl == strlen("chart") && strncmp(t->src + kb, "chart", kl) == 0) {
has_chart = tf_string(t, val, chart, sizeof(chart));
} else if (kl == strlen("repository") && strncmp(t->src + kb, "repository", kl) == 0) {
has_repo = true;
}
}
if (!has_chart) {
return;
}
int64_t src = tf_block_node(t, name);
int64_t tgt = 0;
bool external = false;
char path[CBM_SZ_1K] = "";
static const char OCI[] = "oci://";
if (strncmp(chart, OCI, sizeof(OCI) - SKIP_ONE) == 0) {
const char *base = tf_basename(chart);
if (tf_plain_name(base)) {
snprintf(path, sizeof(path), "%s", base);
external = true;
}
} else if (has_repo) {
if (tf_plain_name(chart)) {
snprintf(path, sizeof(path), "%s", chart);
external = true;
}
} else if (tf_repo_path(t->mdir, chart, path, sizeof(path))) {
tgt = tf_local_chart(t, path);
}
if (external) {
tgt = tf_external_chart(t->ctx, path, t->file);
}
if (src <= 0 || tgt <= 0) {
return;
}
char esc[CBM_SZ_1K];
char props[CBM_SZ_2K];
cbm_json_escape(esc, (int)sizeof(esc), path);
snprintf(props, sizeof(props), "{\"via\":\"terraform\",\"chart\":\"%s\"%s,\"line\":%d}", esc,
external ? ",\"external\":true" : "", (int)ts_node_start_point(block).row + SKIP_ONE);
cbm_gbuf_insert_edge(t->ctx->gbuf, src, tgt, "DEPLOYS", props);
t->edges++;
}
/* The text of a string_lit label: "helm_release" -> helm_release. */
static bool tf_label(const tf_t *t, TSNode n, char *out, size_t sz) {
if (!tf_type(n, "string_lit")) {
return false;
}
for (uint32_t i = 0; i < ts_node_named_child_count(n); i++) {
TSNode c = ts_node_named_child(n, i);
if (tf_type(c, "template_literal")) {
uint32_t b = ts_node_start_byte(c);
snprintf(out, sz, "%.*s", (int)(ts_node_end_byte(c) - b), t->src + b);
return true;
}
}
return false;
}
static void tf_file(tf_t *t, TSNode root) {
TSNode body = {0}; /* comments are children of the file too */
for (uint32_t i = 0; i < ts_node_named_child_count(root) && ts_node_is_null(body); i++) {
if (tf_type(ts_node_named_child(root, i), "body")) {
body = ts_node_named_child(root, i);
}
}
for (uint32_t i = 0; !ts_node_is_null(body) && i < ts_node_named_child_count(body); i++) {
TSNode b = ts_node_named_child(body, i);
if (!tf_type(b, "block") || ts_node_named_child_count(b) < 3) {
continue;
}
TSNode kw = ts_node_named_child(b, 0);
uint32_t kb = ts_node_start_byte(kw);
uint32_t ke = ts_node_end_byte(kw);
char type[CBM_SZ_128];
char name[CBM_SZ_256];
if (ke - kb != strlen("resource") || strncmp(t->src + kb, "resource", ke - kb) != 0 ||
!tf_label(t, ts_node_named_child(b, SKIP_ONE), type, sizeof(type)) ||
!tf_label(t, ts_node_named_child(b, PAIR_LEN), name, sizeof(name))) {
continue;
}
if (strcmp(type, "helm_release") == 0) {
tf_helm_release(t, b, name);
}
}
}
static char *tf_read(const char *path, size_t *len) {
FILE *f = cbm_fopen(path, "rb");
if (!f) {
return NULL;
}
(void)fseek(f, 0, SEEK_END);
long size = ftell(f);
(void)fseek(f, 0, SEEK_SET);
if (size <= 0 || size > cbm_max_file_bytes()) {
(void)fclose(f);
return NULL;
}
enum { TF_TS_PAD = 16 }; /* tree-sitter's lexer reads past the end */
char *buf = cbm_alloc(CBM_MEM_CLASS_OTHER, (size_t)size + TF_TS_PAD);
if (!buf) {
(void)fclose(f);
return NULL;
}
size_t got = fread(buf, SKIP_ONE, (size_t)size, f);
(void)fclose(f);
memset(buf + got, 0, TF_TS_PAD);
*len = got;
return buf;
}
static bool tf_is_module_file(const char *rel) {
const char *dot = strrchr(tf_basename(rel), '.');
return dot && (strcmp(dot, ".tf") == 0 || strcmp(dot, ".tofu") == 0);
}
void cbm_pipeline_pass_terraform(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
int file_count) {
TSParser *parser = NULL;
CBMHashTable *charts = NULL;
int edges = 0;
for (int i = 0; i < file_count; i++) {
if (files[i].language != CBM_LANG_HCL || !tf_is_module_file(files[i].rel_path)) {
continue;
}
size_t len = 0;
char *src = tf_read(files[i].path, &len);
if (!src) {
continue;
}
if (strstr(src, "helm_release")) {
if (!parser) {
parser = ts_parser_new();
charts = tf_chart_index(ctx);
}
TSTree *tree = NULL;
if (parser && ts_parser_set_language(parser, cbm_ts_language(CBM_LANG_HCL))) {
tree = ts_parser_parse_string(parser, NULL, src, (uint32_t)len);
}
if (tree) {
tf_t t = {.ctx = ctx, .charts = charts, .src = src, .file = files[i].rel_path};
const char *slash = strrchr(files[i].rel_path, '/');
snprintf(t.mdir, sizeof(t.mdir), "%.*s",
slash ? (int)(slash - files[i].rel_path) : 0, files[i].rel_path);
tf_file(&t, ts_tree_root_node(tree));
edges += t.edges;
ts_tree_delete(tree);
}
}
cbm_free(CBM_MEM_CLASS_OTHER, src);
}
if (parser) {
ts_parser_delete(parser);
}
if (charts) {
cbm_ht_free(charts);
}
if (edges > 0) {
char buf[CBM_SZ_32];
snprintf(buf, sizeof(buf), "%d", edges);
cbm_log_info("pass.terraform.deploys", "edges", buf);
}
}
+6
View File
@@ -1046,6 +1046,12 @@ int cbm_pipeline_pass_operator(cbm_pipeline_ctx_t *ctx);
* manifests, kustomizations or charts on a cluster DEPLOYS them. Run at the
* end of the K8s pass, over the same files. */
void cbm_pipeline_pass_shell(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, int file_count);
/* Terraform / OpenTofu modules (pass_terraform.c): a helm_release DEPLOYS its
* chart. Run at the end of the K8s pass, over the same files. */
void cbm_pipeline_pass_terraform(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
int file_count);
/* Pre-dump pass: markdown → file REFERENCES_FILE linking. */
int cbm_pipeline_pass_doclinks(cbm_pipeline_ctx_t *ctx);
+35
View File
@@ -1974,6 +1974,40 @@ TEST(probe_make_programs_by_path_spawn) {
PASS();
}
/* A Terraform helm_release DEPLOYS its chart: a chart dir of the module
* (./charts/x, ${path.module}/charts/x), else -- with a repository, or as
* oci://.../<name> -- the external chart of that name. A variable chart or a
* directory without a Chart.yaml is no edge; a file opening with comments is
* read all the same. */
TEST(probe_terraform_helm_release_deploys_its_chart) {
static const GpfFile files[] = {
{"infra/charts/app/Chart.yaml", "apiVersion: v2\nname: app\nversion: 0.1.0\n"},
{"infra/charts/api/Chart.yaml", "apiVersion: v2\nname: api\nversion: 0.1.0\n"},
{"infra/main.tf", "################\n# Helm releases\n################\n\n"
"resource \"helm_release\" \"app\" {\n"
" name = \"app\"\n chart = \"./charts/app\"\n}\n\n"
"resource \"helm_release\" \"api\" {\n"
" name = \"api\"\n chart = \"${path.module}/charts/api\"\n"
" set {\n name = \"replicas\"\n value = \"2\"\n }\n}\n\n"
"resource \"helm_release\" \"cache\" {\n"
" name = \"cache\"\n"
" repository = \"https://charts.example.com\"\n"
" chart = \"redis\"\n}\n\n"
"resource \"helm_release\" \"queue\" {\n"
" name = \"queue\"\n"
" chart = \"oci://registry.example.com/charts/nats\"\n}\n\n"
"resource \"helm_release\" \"dynamic\" {\n"
" name = \"dynamic\"\n chart = var.chart\n}\n\n"
"resource \"helm_release\" \"missing\" {\n"
" name = \"missing\"\n chart = \"./charts/missing\"\n}\n"}};
GPF_ASSERT_DEPLOYS(files, 3,
"resource.helm_release.api -> chart:api (terraform)\n"
"resource.helm_release.app -> chart:app (terraform)\n"
"resource.helm_release.cache -> chart:redis(ext) (terraform)\n"
"resource.helm_release.queue -> chart:nats(ext) (terraform)");
PASS();
}
/* Go code that loads a chart DEPLOYS it: a call of a loader (a function
* reading <param>/Chart.yaml, or the Helm SDK's loader.Load) whose argument
* evaluates over the package's constants, filepath.Join and a local assigned
@@ -3746,6 +3780,7 @@ SUITE(grammar_probe_f) {
RUN_TEST(probe_shell_programs_by_path_spawn);
RUN_TEST(probe_make_programs_by_path_spawn);
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_include_resolves_in_the_chart_family);
RUN_TEST(probe_k8s_selector_stays_in_its_directory);