699 lines
26 KiB
C
699 lines
26 KiB
C
/*
|
|
* pass_k8s.c — Pipeline pass for Kubernetes manifest and Kustomize overlay processing.
|
|
*
|
|
* For each discovered YAML file:
|
|
* 1. Check if it is a kustomize overlay (kustomization.yaml / kustomization.yml)
|
|
* → emit a Module node and IMPORTS edges for each resources/bases/patches entry
|
|
* 2. Else if it is a generic k8s manifest (apiVersion: detected)
|
|
* → emit one Resource node per file (first document only — multi-document YAML is not yet
|
|
* supported)
|
|
*
|
|
* Depends on: pass_infrascan.c (cbm_is_kustomize_file, cbm_is_k8s_manifest, cbm_infra_qn),
|
|
* extraction layer (cbm.h), graph_buffer, pipeline internals.
|
|
*/
|
|
#include "foundation/constants.h"
|
|
#include "pipeline/pipeline.h"
|
|
#include <stdint.h>
|
|
#include "pipeline/pipeline_internal.h"
|
|
#include "graph_buffer/graph_buffer.h"
|
|
#include "discover/discover.h"
|
|
#include "foundation/log.h"
|
|
#include "foundation/compat.h"
|
|
#include "foundation/compat_fs.h"
|
|
#include "foundation/limits.h"
|
|
#include "cbm.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
|
|
/* ── Internal helpers ────────────────────────────────────────────── */
|
|
|
|
/* Read entire file into heap-allocated buffer. Returns NULL on error.
|
|
* Caller must free(). Sets *out_len to byte count. */
|
|
static char *k8s_read_file(const char *path, int *out_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()) { /* generous, env-configurable cap (B4) */
|
|
(void)fclose(f);
|
|
return NULL;
|
|
}
|
|
|
|
/* +pad: tree-sitter lexer lookahead reads past EOF; keep it in-bounds */
|
|
enum { CBM_TS_LOOKAHEAD_PAD = 16 };
|
|
char *buf = malloc((size_t)size + CBM_TS_LOOKAHEAD_PAD);
|
|
if (!buf) {
|
|
(void)fclose(f);
|
|
return NULL;
|
|
}
|
|
|
|
size_t nread = fread(buf, SKIP_ONE, size, f);
|
|
(void)fclose(f);
|
|
if (nread > (size_t)size) {
|
|
nread = (size_t)size;
|
|
}
|
|
memset(buf + nread, 0, CBM_TS_LOOKAHEAD_PAD);
|
|
*out_len = (int)nread;
|
|
return buf;
|
|
}
|
|
|
|
/* Format int to string for logging. Thread-safe via TLS. */
|
|
static const char *itoa_k8s(int val) {
|
|
enum { RING_BUF_COUNT = 4, RING_BUF_MASK = 3 };
|
|
static CBM_TLS char bufs[RING_BUF_COUNT][CBM_SZ_32];
|
|
static CBM_TLS int idx = 0;
|
|
int i = idx;
|
|
idx = (idx + SKIP_ONE) & RING_BUF_MASK;
|
|
snprintf(bufs[i], sizeof(bufs[i]), "%d", val);
|
|
return bufs[i];
|
|
}
|
|
|
|
/* Extract the basename of a path (pointer into the string; no allocation). */
|
|
static const char *k8s_basename(const char *path) {
|
|
const char *p = strrchr(path, '/');
|
|
return p ? p + SKIP_ONE : path;
|
|
}
|
|
|
|
/* ── Kustomize handler ───────────────────────────────────────────── */
|
|
|
|
static void handle_kustomize(cbm_pipeline_ctx_t *ctx, const char *path, const char *rel_path,
|
|
CBMFileResult *result) {
|
|
/* Emit Module node for this kustomize overlay file */
|
|
char *mod_qn = cbm_infra_qn(ctx->project_name, rel_path, "kustomize", NULL);
|
|
if (!mod_qn) {
|
|
return;
|
|
}
|
|
|
|
int64_t mod_id = cbm_gbuf_upsert_node(ctx->gbuf, "Module", k8s_basename(rel_path), mod_qn,
|
|
rel_path, SKIP_ONE, 0, "{\"source\":\"kustomize\"}");
|
|
free(mod_qn);
|
|
|
|
if (mod_id <= 0) {
|
|
return;
|
|
}
|
|
|
|
/* If we have a cached extraction result, emit IMPORTS edges for
|
|
* resources/bases/patches/components entries */
|
|
int import_count = 0;
|
|
CBMFileResult *res = result;
|
|
bool allocated = false;
|
|
|
|
if (!res) {
|
|
/* Fall back to re-extraction */
|
|
int src_len = 0;
|
|
char *source = k8s_read_file(path, &src_len);
|
|
if (source) {
|
|
res = cbm_extract_file(source, src_len, CBM_LANG_KUSTOMIZE, ctx->project_name, rel_path,
|
|
CBM_EXTRACT_BUDGET, NULL, NULL);
|
|
free(source);
|
|
allocated = true;
|
|
}
|
|
}
|
|
|
|
if (res) {
|
|
for (int j = 0; j < res->imports.count; j++) {
|
|
CBMImport *imp = &res->imports.items[j];
|
|
if (!imp->module_path) {
|
|
continue;
|
|
}
|
|
|
|
/* Compute target file QN */
|
|
char *target_qn =
|
|
cbm_pipeline_fqn_compute(ctx->project_name, imp->module_path, "__file__");
|
|
if (!target_qn) {
|
|
continue;
|
|
}
|
|
|
|
const cbm_gbuf_node_t *target = cbm_gbuf_find_by_qn(ctx->gbuf, target_qn);
|
|
free(target_qn);
|
|
|
|
if (target) {
|
|
cbm_gbuf_insert_edge(ctx->gbuf, mod_id, target->id, "IMPORTS",
|
|
"{\"via\":\"kustomize\"}");
|
|
import_count++;
|
|
}
|
|
}
|
|
|
|
if (allocated) {
|
|
cbm_free_result(res);
|
|
}
|
|
}
|
|
|
|
cbm_log_info("pass.k8s.kustomize", "file", rel_path, "imports", itoa_k8s(import_count));
|
|
}
|
|
|
|
/* ── K8s cross-manifest label-selector matching ──────────────────────
|
|
* A Service routes traffic to the workload Pods whose labels match the
|
|
* Service's spec.selector. We record, per manifest, the Resource node id plus
|
|
* its selector label-values (Services) and pod label-values (workloads), then
|
|
* after all manifests are processed connect each Service to the workload(s) it
|
|
* targets via an INFRA_MAPS edge. This models the runtime traffic path the same
|
|
* way k8s itself resolves Service → Endpoints by label.
|
|
*
|
|
* Matching is by label *value*: a Service selector `app: frontend` matches a
|
|
* workload that either carries the pod label value "frontend" OR is named
|
|
* "frontend" (the ubiquitous app==name convention, used when the pod template
|
|
* omits explicit labels). */
|
|
enum { K8S_MAX_LABELS = 16, K8S_MAX_RECORDS = 512, K8S_LABEL_LEN = 128 };
|
|
|
|
typedef struct {
|
|
int64_t node_id;
|
|
char name[K8S_LABEL_LEN];
|
|
bool is_service;
|
|
bool is_workload;
|
|
char selector_vals[K8S_MAX_LABELS][K8S_LABEL_LEN]; /* Service spec.selector values */
|
|
int n_selector;
|
|
char label_vals[K8S_MAX_LABELS][K8S_LABEL_LEN]; /* workload pod-template label values */
|
|
int n_label;
|
|
} k8s_record_t;
|
|
|
|
typedef struct {
|
|
k8s_record_t *items;
|
|
int count;
|
|
int cap;
|
|
} k8s_record_array_t;
|
|
|
|
/* Count leading-space indentation of a line (tabs are invalid YAML indent). */
|
|
static int k8s_indent(const char *line) {
|
|
int n = 0;
|
|
while (line[n] == ' ') {
|
|
n++;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/* Split `key: value` (already de-indented). Returns 1 if a key was found; fills
|
|
* key/val (val empty when the key opens a nested block). */
|
|
static int k8s_split_kv(const char *t, char *key, size_t key_sz, char *val, size_t val_sz) {
|
|
key[0] = '\0';
|
|
val[0] = '\0';
|
|
if (t[0] == '#' || t[0] == '-' || t[0] == '\0') {
|
|
return 0;
|
|
}
|
|
const char *colon = strchr(t, ':');
|
|
if (!colon) {
|
|
return 0;
|
|
}
|
|
size_t klen = (size_t)(colon - t);
|
|
if (klen == 0 || klen >= key_sz) {
|
|
return 0;
|
|
}
|
|
memcpy(key, t, klen);
|
|
key[klen] = '\0';
|
|
const char *v = colon + 1;
|
|
while (*v == ' ' || *v == '\t') {
|
|
v++;
|
|
}
|
|
size_t vn = 0;
|
|
while (v[vn] && v[vn] != '\r' && v[vn] != '\n' && v[vn] != '#' && vn + 1 < val_sz) {
|
|
val[vn] = v[vn];
|
|
vn++;
|
|
}
|
|
/* trim trailing space */
|
|
while (vn > 0 && (val[vn - 1] == ' ' || val[vn - 1] == '\t')) {
|
|
vn--;
|
|
}
|
|
val[vn] = '\0';
|
|
/* strip surrounding quotes */
|
|
if (vn >= 2 && (val[0] == '"' || val[0] == '\'') && val[vn - 1] == val[0]) {
|
|
memmove(val, val + 1, vn - 2);
|
|
val[vn - 2] = '\0';
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static void k8s_add_val(char dst[][K8S_LABEL_LEN], int *n, const char *v) {
|
|
if (*n >= K8S_MAX_LABELS || !v || !v[0]) {
|
|
return;
|
|
}
|
|
snprintf(dst[*n], K8S_LABEL_LEN, "%s", v);
|
|
(*n)++;
|
|
}
|
|
|
|
/* Scan a single-document k8s manifest's text for the resource name, selector
|
|
* label-values (Service) and pod-template label-values (workload). A small
|
|
* indentation path-stack distinguishes spec.selector from
|
|
* spec.template.metadata.labels and from top-level metadata.name. */
|
|
static void k8s_scan_labels(const char *source, k8s_record_t *rec) {
|
|
enum { K8S_PATH_DEPTH = 12 };
|
|
struct {
|
|
int indent;
|
|
char key[64];
|
|
} stack[K8S_PATH_DEPTH];
|
|
int depth = 0;
|
|
bool got_name = false;
|
|
|
|
const char *p = source;
|
|
while (p && *p) {
|
|
const char *eol = strchr(p, '\n');
|
|
size_t len = eol ? (size_t)(eol - p) : strlen(p);
|
|
char line[CBM_SZ_512];
|
|
size_t cp = len < sizeof(line) - 1 ? len : sizeof(line) - 1;
|
|
memcpy(line, p, cp);
|
|
line[cp] = '\0';
|
|
|
|
/* End of first YAML document — stop (one Resource per file). */
|
|
const char *trimmed = line;
|
|
while (*trimmed == ' ') {
|
|
trimmed++;
|
|
}
|
|
if (strncmp(trimmed, "---", 3) == 0 && line == trimmed) {
|
|
break;
|
|
}
|
|
|
|
if (trimmed[0] && trimmed[0] != '#') {
|
|
int ind = k8s_indent(line);
|
|
while (depth > 0 && stack[depth - 1].indent >= ind) {
|
|
depth--;
|
|
}
|
|
char key[64];
|
|
char val[K8S_LABEL_LEN];
|
|
if (k8s_split_kv(trimmed, key, sizeof(key), val, sizeof(val))) {
|
|
/* Build the current dotted path for context decisions. */
|
|
bool under_selector = (depth >= 1 && strcmp(stack[depth - 1].key, "selector") == 0);
|
|
bool under_labels = (depth >= 1 && strcmp(stack[depth - 1].key, "labels") == 0);
|
|
bool under_metadata = (depth >= 1 && strcmp(stack[depth - 1].key, "metadata") == 0);
|
|
|
|
if (val[0] == '\0') {
|
|
/* Block-opening key: push onto the path stack. */
|
|
if (depth < K8S_PATH_DEPTH) {
|
|
stack[depth].indent = ind;
|
|
snprintf(stack[depth].key, sizeof(stack[depth].key), "%s", key);
|
|
depth++;
|
|
}
|
|
} else {
|
|
/* Leaf key: value. */
|
|
if (ind == 0 && strcmp(key, "kind") == 0) {
|
|
rec->is_service = (strcmp(val, "Service") == 0);
|
|
rec->is_workload =
|
|
(strcmp(val, "Deployment") == 0 || strcmp(val, "StatefulSet") == 0 ||
|
|
strcmp(val, "DaemonSet") == 0 || strcmp(val, "ReplicaSet") == 0 ||
|
|
strcmp(val, "ReplicationController") == 0 || strcmp(val, "Pod") == 0 ||
|
|
strcmp(val, "Job") == 0 || strcmp(val, "CronJob") == 0);
|
|
} else if (!got_name && under_metadata && strcmp(key, "name") == 0) {
|
|
snprintf(rec->name, sizeof(rec->name), "%s", val);
|
|
got_name = true;
|
|
} else if (under_selector) {
|
|
/* Service spec.selector matchLabels values. */
|
|
k8s_add_val(rec->selector_vals, &rec->n_selector, val);
|
|
} else if (under_labels) {
|
|
/* Pod-template / metadata labels values. */
|
|
k8s_add_val(rec->label_vals, &rec->n_label, val);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!eol) {
|
|
break;
|
|
}
|
|
p = eol + 1;
|
|
}
|
|
}
|
|
|
|
/* True if any of the service's selector values matches the workload. */
|
|
static bool k8s_selector_matches(const k8s_record_t *svc, const k8s_record_t *wl) {
|
|
for (int s = 0; s < svc->n_selector; s++) {
|
|
const char *sv = svc->selector_vals[s];
|
|
if (!sv[0]) {
|
|
continue;
|
|
}
|
|
if (wl->name[0] && strcmp(sv, wl->name) == 0) {
|
|
return true;
|
|
}
|
|
for (int l = 0; l < wl->n_label; l++) {
|
|
if (strcmp(sv, wl->label_vals[l]) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* After all manifests are recorded, connect each Service to the workload(s) its
|
|
* selector targets via an INFRA_MAPS edge (Service Resource → workload Resource). */
|
|
static void k8s_link_selectors(cbm_pipeline_ctx_t *ctx, const k8s_record_array_t *recs) {
|
|
int edges = 0;
|
|
for (int i = 0; i < recs->count; i++) {
|
|
const k8s_record_t *svc = &recs->items[i];
|
|
if (!svc->is_service || svc->n_selector == 0 || svc->node_id <= 0) {
|
|
continue;
|
|
}
|
|
for (int j = 0; j < recs->count; j++) {
|
|
const k8s_record_t *wl = &recs->items[j];
|
|
if (i == j || !wl->is_workload || wl->node_id <= 0) {
|
|
continue;
|
|
}
|
|
if (k8s_selector_matches(svc, wl)) {
|
|
char props[CBM_SZ_256];
|
|
snprintf(props, sizeof(props),
|
|
"{\"kind\":\"selector\",\"service\":\"%s\",\"workload\":\"%s\"}",
|
|
svc->name, wl->name);
|
|
cbm_gbuf_insert_edge(ctx->gbuf, svc->node_id, wl->node_id, "INFRA_MAPS", props);
|
|
edges++;
|
|
}
|
|
}
|
|
}
|
|
if (edges > 0) {
|
|
cbm_log_info("pass.k8s.selectors", "linked", itoa_k8s(edges));
|
|
}
|
|
}
|
|
|
|
/* ── K8s manifest handler ────────────────────────────────────────── */
|
|
|
|
/* source/src_len are the already-read file bytes (caller retains ownership and
|
|
* must free after this call returns). When `rec` is non-NULL it is populated
|
|
* with the first Resource's node id, name and label/selector values for later
|
|
* cross-manifest selector matching. */
|
|
static void handle_k8s_manifest(cbm_pipeline_ctx_t *ctx, const char *path, const char *rel_path,
|
|
const char *source, int src_len, k8s_record_t *rec) {
|
|
(void)path; /* retained for symmetry; source is always provided now */
|
|
int resource_count = 0;
|
|
|
|
CBMFileResult *res = cbm_extract_file(source, src_len, CBM_LANG_K8S, ctx->project_name,
|
|
rel_path, CBM_EXTRACT_BUDGET, NULL, NULL);
|
|
if (!res) {
|
|
return;
|
|
}
|
|
|
|
/* Compute file node QN for DEFINES edges */
|
|
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel_path, "__file__");
|
|
const cbm_gbuf_node_t *file_node = file_qn ? cbm_gbuf_find_by_qn(ctx->gbuf, file_qn) : NULL;
|
|
free(file_qn);
|
|
|
|
for (int d = 0; d < res->defs.count; d++) {
|
|
CBMDefinition *def = &res->defs.items[d];
|
|
if (!def->label || strcmp(def->label, "Resource") != 0) {
|
|
continue;
|
|
}
|
|
if (!def->name || !def->qualified_name) {
|
|
continue;
|
|
}
|
|
|
|
int64_t node_id =
|
|
cbm_gbuf_upsert_node(ctx->gbuf, "Resource", def->name, def->qualified_name, rel_path,
|
|
(int)def->start_line, (int)def->end_line, "{\"source\":\"k8s\"}");
|
|
|
|
/* DEFINES edge: File → Resource */
|
|
if (file_node && node_id > 0) {
|
|
cbm_gbuf_insert_edge(ctx->gbuf, file_node->id, node_id, "DEFINES", "{}");
|
|
}
|
|
|
|
/* Capture the first Resource for cross-manifest selector matching. */
|
|
if (rec && rec->node_id <= 0 && node_id > 0) {
|
|
rec->node_id = node_id;
|
|
}
|
|
|
|
resource_count++;
|
|
}
|
|
|
|
cbm_free_result(res);
|
|
|
|
/* Record selector / pod-label values for later Service → workload linking. */
|
|
if (rec && rec->node_id > 0) {
|
|
k8s_scan_labels(source, rec);
|
|
}
|
|
|
|
cbm_log_info("pass.k8s.manifest", "file", rel_path, "resources", itoa_k8s(resource_count));
|
|
}
|
|
|
|
/* ── Helm chart handler ──────────────────────────────────────────── */
|
|
|
|
static bool is_helm_chart_file(const char *base) {
|
|
return strcmp(base, "Chart.yaml") == 0 || strcmp(base, "Chart.yml") == 0;
|
|
}
|
|
|
|
/* Emit a Chart node for a Chart.yaml and a DEPENDS_ON edge to a (shared,
|
|
* deduplicated) Chart node per declared dependency (#338). */
|
|
static void handle_helm_chart(cbm_pipeline_ctx_t *ctx, const char *rel_path, const char *source) {
|
|
cbm_helm_chart_t hc;
|
|
if (cbm_parse_helm_chart(source, &hc) != 0) {
|
|
return;
|
|
}
|
|
|
|
const char *cname = hc.chart_name[0] ? hc.chart_name : k8s_basename(rel_path);
|
|
char *chart_qn = cbm_infra_qn(ctx->project_name, rel_path, "helm-chart", NULL);
|
|
if (!chart_qn) {
|
|
return;
|
|
}
|
|
int64_t chart_id = cbm_gbuf_upsert_node(ctx->gbuf, "Chart", cname, chart_qn, rel_path, SKIP_ONE,
|
|
0, "{\"source\":\"helm\"}");
|
|
free(chart_qn);
|
|
|
|
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel_path, "__file__");
|
|
const cbm_gbuf_node_t *file_node = file_qn ? cbm_gbuf_find_by_qn(ctx->gbuf, file_qn) : NULL;
|
|
if (file_node && chart_id > 0) {
|
|
cbm_gbuf_insert_edge(ctx->gbuf, file_node->id, chart_id, "DEFINES", "{}");
|
|
}
|
|
free(file_qn);
|
|
|
|
int dep_edges = 0;
|
|
for (int i = 0; i < hc.dep_count && chart_id > 0; i++) {
|
|
/* Stable per-project QN so multiple charts depending on the same chart
|
|
* link to one shared dependency node. */
|
|
char dep_qn[CBM_SZ_512];
|
|
snprintf(dep_qn, sizeof(dep_qn), "%s.__helm_dep__.%s", ctx->project_name, hc.deps[i]);
|
|
int64_t dep_id =
|
|
cbm_gbuf_upsert_node(ctx->gbuf, "Chart", hc.deps[i], dep_qn, rel_path, SKIP_ONE, 0,
|
|
"{\"source\":\"helm\",\"external\":true}");
|
|
if (dep_id > 0) {
|
|
cbm_gbuf_insert_edge(ctx->gbuf, chart_id, dep_id, "DEPENDS_ON", "{}");
|
|
dep_edges++;
|
|
}
|
|
}
|
|
cbm_log_info("pass.k8s.helm", "file", rel_path, "deps", itoa_k8s(dep_edges));
|
|
}
|
|
|
|
/* ── Dependency-manifest handler (go.mod / requirements.txt) ──────── */
|
|
|
|
static bool is_gomod_file(const char *base) {
|
|
return strcmp(base, "go.mod") == 0;
|
|
}
|
|
|
|
static bool is_requirements_file(const char *base) {
|
|
return strcmp(base, "requirements.txt") == 0;
|
|
}
|
|
|
|
/* Emit a DEPENDS_ON edge from the manifest file node to a (shared, per-project)
|
|
* external Package node. Mirrors the Helm Chart.yaml DEPENDS_ON shape. */
|
|
static int emit_dep_edge(cbm_pipeline_ctx_t *ctx, const cbm_gbuf_node_t *src, const char *rel_path,
|
|
const char *ecosystem, const char *name) {
|
|
if (!name || !name[0]) {
|
|
return 0;
|
|
}
|
|
char dep_qn[CBM_SZ_512];
|
|
snprintf(dep_qn, sizeof(dep_qn), "%s.__%s_dep__.%s", ctx->project_name, ecosystem, name);
|
|
char dep_props[CBM_SZ_256];
|
|
snprintf(dep_props, sizeof(dep_props), "{\"source\":\"%s\",\"external\":true}", ecosystem);
|
|
int64_t dep_id =
|
|
cbm_gbuf_upsert_node(ctx->gbuf, "Package", name, dep_qn, rel_path, SKIP_ONE, 0, dep_props);
|
|
if (dep_id > 0 && dep_id != src->id) {
|
|
cbm_gbuf_insert_edge(ctx->gbuf, src->id, dep_id, "DEPENDS_ON", "{}");
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Copy the first whitespace-delimited token of `line` into `out`. */
|
|
static void first_token(const char *line, char *out, size_t out_sz) {
|
|
out[0] = '\0';
|
|
while (*line == ' ' || *line == '\t') {
|
|
line++;
|
|
}
|
|
size_t n = 0;
|
|
while (line[n] && line[n] != ' ' && line[n] != '\t' && line[n] != '\r' && line[n] != '\n' &&
|
|
n + 1 < out_sz) {
|
|
out[n] = line[n];
|
|
n++;
|
|
}
|
|
out[n] = '\0';
|
|
}
|
|
|
|
/* Parse go.mod `require` directives (single-line and block forms) and emit a
|
|
* DEPENDS_ON edge per dependency. go.mod requires are not surfaced as imports
|
|
* by the extraction layer, so we parse the manifest text directly here. */
|
|
static int parse_gomod_deps(cbm_pipeline_ctx_t *ctx, const cbm_gbuf_node_t *src,
|
|
const char *rel_path, const char *source) {
|
|
int edges = 0;
|
|
bool in_block = false;
|
|
const char *p = source;
|
|
while (p && *p) {
|
|
const char *eol = strchr(p, '\n');
|
|
size_t len = eol ? (size_t)(eol - p) : strlen(p);
|
|
char line[CBM_SZ_512];
|
|
size_t cp = len < sizeof(line) - 1 ? len : sizeof(line) - 1;
|
|
memcpy(line, p, cp);
|
|
line[cp] = '\0';
|
|
const char *t = line;
|
|
while (*t == ' ' || *t == '\t') {
|
|
t++;
|
|
}
|
|
if (in_block) {
|
|
if (t[0] == ')') {
|
|
in_block = false;
|
|
} else if (t[0] && t[0] != '/') {
|
|
char name[CBM_SZ_256];
|
|
first_token(t, name, sizeof(name));
|
|
edges += emit_dep_edge(ctx, src, rel_path, "gomod", name);
|
|
}
|
|
} else if (strncmp(t, "require", 7) == 0 && (t[7] == ' ' || t[7] == '\t' || t[7] == '(')) {
|
|
const char *rest = t + 7;
|
|
while (*rest == ' ' || *rest == '\t') {
|
|
rest++;
|
|
}
|
|
if (*rest == '(') {
|
|
in_block = true;
|
|
} else if (*rest) {
|
|
char name[CBM_SZ_256];
|
|
first_token(rest, name, sizeof(name));
|
|
edges += emit_dep_edge(ctx, src, rel_path, "gomod", name);
|
|
}
|
|
}
|
|
if (!eol) {
|
|
break;
|
|
}
|
|
p = eol + 1;
|
|
}
|
|
return edges;
|
|
}
|
|
|
|
/* Parse requirements.txt entries (one package spec per line) and emit a
|
|
* DEPENDS_ON edge per dependency. The package name is the leading token up to
|
|
* the first version/extras/comment delimiter. */
|
|
static int parse_requirements_deps(cbm_pipeline_ctx_t *ctx, const cbm_gbuf_node_t *src,
|
|
const char *rel_path, const char *source) {
|
|
int edges = 0;
|
|
const char *p = source;
|
|
while (p && *p) {
|
|
const char *eol = strchr(p, '\n');
|
|
size_t len = eol ? (size_t)(eol - p) : strlen(p);
|
|
char line[CBM_SZ_512];
|
|
size_t cp = len < sizeof(line) - 1 ? len : sizeof(line) - 1;
|
|
memcpy(line, p, cp);
|
|
line[cp] = '\0';
|
|
const char *t = line;
|
|
while (*t == ' ' || *t == '\t') {
|
|
t++;
|
|
}
|
|
/* Skip blanks, comments, options (-r, --hash), and URLs. */
|
|
if (t[0] && t[0] != '#' && t[0] != '-' && strstr(t, "://") == NULL) {
|
|
char name[CBM_SZ_256];
|
|
size_t n = 0;
|
|
while (t[n] && t[n] != '=' && t[n] != '<' && t[n] != '>' && t[n] != '!' &&
|
|
t[n] != '~' && t[n] != '[' && t[n] != ';' && t[n] != ' ' && t[n] != '\t' &&
|
|
t[n] != '\r' && n + 1 < sizeof(name)) {
|
|
name[n] = t[n];
|
|
n++;
|
|
}
|
|
name[n] = '\0';
|
|
edges += emit_dep_edge(ctx, src, rel_path, "pypi", name);
|
|
}
|
|
if (!eol) {
|
|
break;
|
|
}
|
|
p = eol + 1;
|
|
}
|
|
return edges;
|
|
}
|
|
|
|
static void handle_dep_manifest(cbm_pipeline_ctx_t *ctx, const char *rel_path, const char *source,
|
|
const char *ecosystem) {
|
|
if (!source) {
|
|
return;
|
|
}
|
|
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel_path, "__file__");
|
|
const cbm_gbuf_node_t *src = file_qn ? cbm_gbuf_find_by_qn(ctx->gbuf, file_qn) : NULL;
|
|
free(file_qn);
|
|
if (!src) {
|
|
return;
|
|
}
|
|
int dep_edges = strcmp(ecosystem, "gomod") == 0
|
|
? parse_gomod_deps(ctx, src, rel_path, source)
|
|
: parse_requirements_deps(ctx, src, rel_path, source);
|
|
cbm_log_info("pass.k8s.depmanifest", "file", rel_path, "deps", itoa_k8s(dep_edges));
|
|
}
|
|
|
|
/* ── Pass entry point ────────────────────────────────────────────── */
|
|
|
|
int cbm_pipeline_pass_k8s(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, int file_count) {
|
|
cbm_log_info("pass.start", "pass", "k8s", "files", itoa_k8s(file_count));
|
|
|
|
cbm_init();
|
|
|
|
int kustomize_count = 0;
|
|
int manifest_count = 0;
|
|
int helm_count = 0;
|
|
|
|
/* Collect per-manifest selector/label records for cross-manifest matching. */
|
|
k8s_record_array_t recs = {0};
|
|
recs.items = calloc(K8S_MAX_RECORDS, sizeof(*recs.items));
|
|
recs.cap = recs.items ? K8S_MAX_RECORDS : 0;
|
|
|
|
for (int i = 0; i < file_count; i++) {
|
|
if (cbm_pipeline_check_cancel(ctx)) {
|
|
free(recs.items);
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
|
|
const char *path = files[i].path;
|
|
const char *rel = files[i].rel_path;
|
|
CBMLanguage lang = files[i].language;
|
|
const char *base = k8s_basename(rel);
|
|
|
|
CBMFileResult *cached =
|
|
(ctx->result_cache && ctx->result_cache[i]) ? ctx->result_cache[i] : NULL;
|
|
|
|
if (is_gomod_file(base) || lang == CBM_LANG_GOMOD || is_requirements_file(base)) {
|
|
int dep_len = 0;
|
|
char *dep_src = k8s_read_file(path, &dep_len);
|
|
if (dep_src) {
|
|
handle_dep_manifest(ctx, rel, dep_src,
|
|
is_requirements_file(base) ? "pypi" : "gomod");
|
|
free(dep_src);
|
|
}
|
|
} else if (cbm_is_kustomize_file(base)) {
|
|
handle_kustomize(ctx, path, rel, cached);
|
|
kustomize_count++;
|
|
} else if (lang == CBM_LANG_YAML || lang == CBM_LANG_K8S) {
|
|
/* Read source once to classify (and reuse for uncached extraction). */
|
|
int src_len = 0;
|
|
char *source = k8s_read_file(path, &src_len);
|
|
if (source) {
|
|
if (is_helm_chart_file(base)) {
|
|
handle_helm_chart(ctx, rel, source);
|
|
helm_count++;
|
|
} else if (cbm_is_k8s_manifest(base, source)) {
|
|
/* Always re-extract with CBM_LANG_K8S regardless of any cached
|
|
* result: cached results were produced during the parallel YAML
|
|
* pass and contain no "Resource" definitions. Pass the already-
|
|
* read source buffer so handle_k8s_manifest does not re-read. */
|
|
(void)cached; /* cached YAML result intentionally discarded */
|
|
k8s_record_t *rec = (recs.count < recs.cap) ? &recs.items[recs.count] : NULL;
|
|
handle_k8s_manifest(ctx, path, rel, source, src_len, rec);
|
|
if (rec && rec->node_id > 0) {
|
|
recs.count++;
|
|
}
|
|
manifest_count++;
|
|
}
|
|
free(source);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Connect Services to the workloads their selectors target (INFRA_MAPS). */
|
|
k8s_link_selectors(ctx, &recs);
|
|
free(recs.items);
|
|
|
|
cbm_log_info("pass.done", "pass", "k8s", "kustomize", itoa_k8s(kustomize_count), "manifests",
|
|
itoa_k8s(manifest_count));
|
|
(void)helm_count;
|
|
return 0;
|
|
}
|