Skip to content

Commit bf3af55

Browse files
feat(k8s): pipeline pass for k8s manifest parsing and graph node emission
- Add pass_k8s.c: cbm_pipeline_pass_k8s() iterates files, classifies kustomize overlays via cbm_is_kustomize_file() and k8s manifests via cbm_is_k8s_manifest(), emits Module/Resource nodes and IMPORTS/DEFINES edges - Kustomize files emit Module node (cbm_infra_qn) + IMPORTS edges per resources entry - K8s manifest files emit Resource nodes per top-level document with DEFINES edge - Falls back to file re-read + re-extraction when result_cache is unavailable - Declare cbm_pipeline_pass_k8s() prototype in pipeline_internal.h - Add pass_k8s.c to PIPELINE_SRCS in Makefile.cbm - Call pass after definitions pass in pipeline.c sequential path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 88ddb63 commit bf3af55

4 files changed

Lines changed: 281 additions & 0 deletions

File tree

Makefile.cbm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ PIPELINE_SRCS = \
177177
src/pipeline/pass_envscan.c \
178178
src/pipeline/pass_compile_commands.c \
179179
src/pipeline/pass_infrascan.c \
180+
src/pipeline/pass_k8s.c \
180181
src/pipeline/httplink.c
181182

182183
# Traces module (new)

src/pipeline/pass_k8s.c

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
/*
2+
* pass_k8s.c — Pipeline pass for Kubernetes manifest and Kustomize overlay processing.
3+
*
4+
* For each discovered YAML file:
5+
* 1. Check if it is a kustomize overlay (kustomization.yaml / kustomization.yml)
6+
* → emit a Module node and IMPORTS edges for each resources/bases/patches entry
7+
* 2. Else if it is a generic k8s manifest (apiVersion: detected)
8+
* → emit one Resource node per top-level resource document
9+
*
10+
* Depends on: pass_infrascan.c (cbm_is_kustomize_file, cbm_is_k8s_manifest, cbm_infra_qn),
11+
* extraction layer (cbm.h), graph_buffer, pipeline internals.
12+
*/
13+
#include "pipeline/pipeline.h"
14+
#include "pipeline/pipeline_internal.h"
15+
#include "graph_buffer/graph_buffer.h"
16+
#include "discover/discover.h"
17+
#include "foundation/log.h"
18+
#include "cbm.h"
19+
20+
#include <stdlib.h>
21+
#include <string.h>
22+
#include <stdio.h>
23+
24+
/* ── Internal helpers ────────────────────────────────────────────── */
25+
26+
/* Read entire file into heap-allocated buffer. Returns NULL on error.
27+
* Caller must free(). Sets *out_len to byte count. */
28+
static char *k8s_read_file(const char *path, int *out_len) {
29+
FILE *f = fopen(path, "rb");
30+
if (!f) {
31+
return NULL;
32+
}
33+
34+
(void)fseek(f, 0, SEEK_END);
35+
long size = ftell(f);
36+
(void)fseek(f, 0, SEEK_SET);
37+
38+
if (size <= 0 || size > (long)100 * 1024 * 1024) {
39+
(void)fclose(f);
40+
return NULL;
41+
}
42+
43+
char *buf = malloc(size + 1);
44+
if (!buf) {
45+
(void)fclose(f);
46+
return NULL;
47+
}
48+
49+
size_t nread = fread(buf, 1, size, f);
50+
(void)fclose(f);
51+
// NOLINTNEXTLINE(clang-analyzer-security.ArrayBound)
52+
buf[nread] = '\0';
53+
*out_len = (int)nread;
54+
return buf;
55+
}
56+
57+
/* Format int to string for logging. Thread-safe via TLS. */
58+
static const char *itoa_k8s(int val) {
59+
static CBM_TLS char bufs[4][32];
60+
static CBM_TLS int idx = 0;
61+
int i = idx;
62+
idx = (idx + 1) & 3;
63+
snprintf(bufs[i], sizeof(bufs[i]), "%d", val);
64+
return bufs[i];
65+
}
66+
67+
/* Extract the basename of a path (pointer into the string; no allocation). */
68+
static const char *k8s_basename(const char *path) {
69+
const char *p = strrchr(path, '/');
70+
return p ? p + 1 : path;
71+
}
72+
73+
/* ── Kustomize handler ───────────────────────────────────────────── */
74+
75+
static void handle_kustomize(cbm_pipeline_ctx_t *ctx, const char *path, const char *rel_path,
76+
CBMFileResult *result) {
77+
/* Emit Module node for this kustomize overlay file */
78+
char *mod_qn = cbm_infra_qn(ctx->project_name, rel_path, "kustomize", NULL);
79+
if (!mod_qn) {
80+
return;
81+
}
82+
83+
// NOLINTNEXTLINE(misc-include-cleaner)
84+
int64_t mod_id =
85+
cbm_gbuf_upsert_node(ctx->gbuf, "Module", k8s_basename(rel_path), mod_qn, rel_path, 1, 0,
86+
"{\"source\":\"kustomize\"}");
87+
free(mod_qn);
88+
89+
if (mod_id <= 0) {
90+
return;
91+
}
92+
93+
/* If we have a cached extraction result, emit IMPORTS edges for
94+
* resources/bases/patches/components entries */
95+
int import_count = 0;
96+
CBMFileResult *res = result;
97+
bool allocated = false;
98+
99+
if (!res) {
100+
/* Fall back to re-extraction */
101+
int src_len = 0;
102+
char *source = k8s_read_file(path, &src_len);
103+
if (source) {
104+
res = cbm_extract_file(source, src_len, CBM_LANG_KUSTOMIZE, ctx->project_name, rel_path,
105+
CBM_EXTRACT_BUDGET, NULL, NULL);
106+
free(source);
107+
allocated = true;
108+
}
109+
}
110+
111+
if (res) {
112+
for (int j = 0; j < res->imports.count; j++) {
113+
CBMImport *imp = &res->imports.items[j];
114+
if (!imp->module_path) {
115+
continue;
116+
}
117+
118+
/* Compute target file QN */
119+
char *target_qn =
120+
cbm_pipeline_fqn_compute(ctx->project_name, imp->module_path, "__file__");
121+
if (!target_qn) {
122+
continue;
123+
}
124+
125+
const cbm_gbuf_node_t *target = cbm_gbuf_find_by_qn(ctx->gbuf, target_qn);
126+
free(target_qn);
127+
128+
if (target) {
129+
cbm_gbuf_insert_edge(ctx->gbuf, mod_id, target->id, "IMPORTS",
130+
"{\"via\":\"kustomize\"}");
131+
import_count++;
132+
}
133+
}
134+
135+
if (allocated) {
136+
cbm_free_result(res);
137+
}
138+
}
139+
140+
cbm_log_info("pass.k8s.kustomize", "file", rel_path, "imports", itoa_k8s(import_count));
141+
}
142+
143+
/* ── K8s manifest handler ────────────────────────────────────────── */
144+
145+
static void handle_k8s_manifest(cbm_pipeline_ctx_t *ctx, const char *path, const char *rel_path,
146+
CBMFileResult *result) {
147+
int resource_count = 0;
148+
CBMFileResult *res = result;
149+
bool allocated = false;
150+
151+
if (!res) {
152+
/* Fall back to re-extraction */
153+
int src_len = 0;
154+
char *source = k8s_read_file(path, &src_len);
155+
if (source) {
156+
res = cbm_extract_file(source, src_len, CBM_LANG_K8S, ctx->project_name, rel_path,
157+
CBM_EXTRACT_BUDGET, NULL, NULL);
158+
free(source);
159+
allocated = true;
160+
}
161+
}
162+
163+
if (!res) {
164+
return;
165+
}
166+
167+
/* Compute file node QN for DEFINES edges */
168+
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel_path, "__file__");
169+
const cbm_gbuf_node_t *file_node = file_qn ? cbm_gbuf_find_by_qn(ctx->gbuf, file_qn) : NULL;
170+
free(file_qn);
171+
172+
for (int d = 0; d < res->defs.count; d++) {
173+
CBMDefinition *def = &res->defs.items[d];
174+
if (!def->label || strcmp(def->label, "Resource") != 0) {
175+
continue;
176+
}
177+
if (!def->name || !def->qualified_name) {
178+
continue;
179+
}
180+
181+
// NOLINTNEXTLINE(misc-include-cleaner)
182+
int64_t node_id =
183+
cbm_gbuf_upsert_node(ctx->gbuf, "Resource", def->name, def->qualified_name, rel_path,
184+
(int)def->start_line, (int)def->end_line, "{\"source\":\"k8s\"}");
185+
186+
/* DEFINES edge: File → Resource */
187+
if (file_node && node_id > 0) {
188+
cbm_gbuf_insert_edge(ctx->gbuf, file_node->id, node_id, "DEFINES", "{}");
189+
}
190+
191+
resource_count++;
192+
}
193+
194+
if (allocated) {
195+
cbm_free_result(res);
196+
}
197+
198+
cbm_log_info("pass.k8s.manifest", "file", rel_path, "resources", itoa_k8s(resource_count));
199+
}
200+
201+
/* ── Pass entry point ────────────────────────────────────────────── */
202+
203+
// NOLINTNEXTLINE(misc-include-cleaner) — cbm_file_info_t provided by standard header
204+
int cbm_pipeline_pass_k8s(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, int file_count) {
205+
cbm_log_info("pass.start", "pass", "k8s", "files", itoa_k8s(file_count));
206+
207+
cbm_init();
208+
209+
int kustomize_count = 0;
210+
int manifest_count = 0;
211+
212+
for (int i = 0; i < file_count; i++) {
213+
if (cbm_pipeline_check_cancel(ctx)) {
214+
return -1;
215+
}
216+
217+
const char *path = files[i].path;
218+
const char *rel = files[i].rel_path;
219+
CBMLanguage lang = files[i].language;
220+
const char *base = k8s_basename(rel);
221+
222+
CBMFileResult *cached = (ctx->result_cache && ctx->result_cache[i])
223+
? ctx->result_cache[i]
224+
: NULL;
225+
226+
if (cbm_is_kustomize_file(base)) {
227+
handle_kustomize(ctx, path, rel, cached);
228+
kustomize_count++;
229+
} else if (lang == CBM_LANG_YAML || lang == CBM_LANG_K8S) {
230+
/* Need source content for cbm_is_k8s_manifest check */
231+
if (cached) {
232+
/* Use cached result — the file is a k8s manifest if lang is CBM_LANG_K8S,
233+
* or if we check the source. With a cached result available, trust the
234+
* language field set during discovery. */
235+
if (lang == CBM_LANG_K8S) {
236+
handle_k8s_manifest(ctx, path, rel, cached);
237+
manifest_count++;
238+
} else {
239+
/* CBM_LANG_YAML: need source to confirm apiVersion presence */
240+
int src_len = 0;
241+
char *source = k8s_read_file(path, &src_len);
242+
if (source) {
243+
if (cbm_is_k8s_manifest(base, source)) {
244+
handle_k8s_manifest(ctx, path, rel, cached);
245+
manifest_count++;
246+
}
247+
free(source);
248+
}
249+
}
250+
} else {
251+
/* No cached result — read source to classify */
252+
int src_len = 0;
253+
char *source = k8s_read_file(path, &src_len);
254+
if (source) {
255+
if (cbm_is_k8s_manifest(base, source)) {
256+
/* Pass NULL result — handle_k8s_manifest will re-extract */
257+
handle_k8s_manifest(ctx, path, rel, NULL);
258+
manifest_count++;
259+
}
260+
free(source);
261+
}
262+
}
263+
}
264+
}
265+
266+
cbm_log_info("pass.done", "pass", "k8s", "kustomize", itoa_k8s(kustomize_count), "manifests",
267+
itoa_k8s(manifest_count));
268+
return 0;
269+
}

src/pipeline/pipeline.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,15 @@ int cbm_pipeline_run(cbm_pipeline_t *p) {
518518
goto seq_cleanup;
519519
}
520520

521+
cbm_clock_gettime(CLOCK_MONOTONIC, &t);
522+
rc = cbm_pipeline_pass_k8s(&ctx, files, file_count);
523+
if (rc != 0) { /* log warning, continue */ }
524+
cbm_log_info("pass.timing", "pass", "k8s", "elapsed_ms", itoa_buf((int)elapsed_ms(t)));
525+
if (check_cancel(p)) {
526+
rc = -1;
527+
goto seq_cleanup;
528+
}
529+
521530
cbm_clock_gettime(CLOCK_MONOTONIC, &t);
522531
rc = cbm_pipeline_pass_calls(&ctx, files, file_count);
523532
if (rc != 0) {

src/pipeline/pipeline_internal.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,8 @@ int cbm_parallel_resolve(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
355355
int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
356356
int file_count);
357357

358+
int cbm_pipeline_pass_k8s(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, int file_count);
359+
358360
int cbm_pipeline_pass_calls(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, int file_count);
359361

360362
/* Sub-passes called from pass_calls: pattern-based edge extraction */

0 commit comments

Comments
 (0)