fix(k8s): address QA round 1 findings

- [Major 1+2] pass_k8s: always re-extract K8s manifests with CBM_LANG_K8S,
  discarding any cached YAML result; pass already-read source buffer to
  handle_k8s_manifest to eliminate the double file read
- [Minor 3] CONTRIBUTING.md: fix extractor path to internal/cbm/extract_k8s.c
  and clarify tree-sitter YAML grammar usage
- [Minor 4] language.c: add comment documenting intentional case-sensitive
  FILENAME_TABLE and case-insensitive cbm_is_kustomize_file() split behaviour
- [Minor 5] test_pipeline.c: add k8s_extract_manifest_multidoc test pinning
  single-document-per-file extraction behaviour
- [Minor 6] pass_infrascan: replace strlen with strnlen(content, 4096) in
  cbm_is_k8s_manifest to bound the scan
- [Minor 7] extract_k8s: add "crds" to is_kustomize_list_key
- [Nit 8] extract_k8s: remove dead second block_node unwrap in
  extract_k8s_scalars metadata descent
- [Nit 10] extract_k8s: use cbm_arena_strdup for def.label "Resource"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Shane McCarronandClaude Sonnet 4.6 committed 2026-03-21 09:29:22 -05:00
1 parent e696fcdf80
commit 94841e0542
6 files changed
+69 -58

No files matched your search

+1 -1
View File
@@ -89,7 +89,7 @@ Language support is split between two layers:
Languages like **Dockerfile**, **docker-compose**, **Kubernetes manifests**, and **Kustomize** do not use tree-sitter grammars. Instead they follow an *infra-pass* pattern:
1. **Detection helpers** in `src/pipeline/pass_infrascan.c` — functions like `cbm_is_dockerfile()`, `cbm_is_k8s_manifest()`, `cbm_is_kustomize_file()` identify files by name and/or content heuristics (e.g., presence of `apiVersion:`).
2. **Custom extractors** in `src/pipeline/extract_k8s.c` (or `extract_infra.c`) — hand-written parsers that walk the raw YAML/text and populate `CBMFileResult` with imports and definitions.
2. **Custom extractors** in `internal/cbm/extract_k8s.c` — tree-sitter-based parsers that walk the YAML AST (using the tree-sitter YAML grammar) and populate `CBMFileResult` with imports and definitions.
3. **Pipeline pass** (`pass_k8s.c`, `pass_infrascan.c`) — calls the extractor and emits graph nodes/edges. K8s manifests emit `Resource` nodes; Kustomize files emit `Module` nodes with `IMPORTS` edges to referenced resource files.
**When adding a new infrastructure language:**
+3 -5
View File
@@ -57,7 +57,7 @@ static const char *get_scalar_text(CBMArena *a, TSNode node, const char *source)
static int is_kustomize_list_key(const char *key) {
return (strcmp(key, "resources") == 0 || strcmp(key, "bases") == 0 ||
strcmp(key, "patches") == 0 || strcmp(key, "components") == 0 ||
strcmp(key, "patchesStrategicMerge") == 0);
strcmp(key, "patchesStrategicMerge") == 0 || strcmp(key, "crds") == 0);
}
// ---------------------------------------------------------------------------
@@ -196,10 +196,8 @@ static void extract_k8s_scalars(CBMExtractCtx *ctx, TSNode mapping, char *kind_b
}
} else if (strcmp(key, "metadata") == 0) {
// Descend into metadata block_mapping to find "name"
// val_node is already unwrapped from block_node above.
TSNode meta_mapping = val_node;
if (strcmp(ts_node_type(meta_mapping), "block_node") == 0) {
meta_mapping = ts_node_named_child(meta_mapping, 0);
}
if (ts_node_is_null(meta_mapping) ||
strcmp(ts_node_type(meta_mapping), "block_mapping") != 0) {
continue;
@@ -269,7 +267,7 @@ static void extract_k8s_manifest(CBMExtractCtx *ctx) {
CBMDefinition def = {0};
def.name = cbm_arena_strdup(a, def_name);
def.qualified_name = cbm_arena_sprintf(a, "%s.%s", ctx->module_qn, def_name);
def.label = "Resource";
def.label = cbm_arena_strdup(a, "Resource");
def.file_path = ctx->rel_path;
def.start_line = ts_node_start_point(mapping).row + 1;
def.end_line = ts_node_end_point(mapping).row + 1;
+5
View File
@@ -279,6 +279,11 @@ static const filename_entry_t FILENAME_TABLE[] = {
{"meson_options.txt", CBM_LANG_MESON},
{"kustomization.yaml", CBM_LANG_KUSTOMIZE},
{"kustomization.yml", CBM_LANG_KUSTOMIZE},
/* Note: FILENAME_TABLE uses case-sensitive strcmp, so mixed-case variants
* (e.g. "Kustomization.yaml") are not matched here. They fall through to
* CBM_LANG_YAML and are re-classified by cbm_is_kustomize_file() in
* pass_k8s.c, which performs a case-insensitive comparison. This is the
* intended behaviour — no additional entries are needed. */
{".vimrc", CBM_LANG_VIMSCRIPT},
};
+1 -4
View File
@@ -207,10 +207,7 @@ bool cbm_is_k8s_manifest(const char *name, const char *content) {
return false;
}
char buf[4097];
size_t n = strlen(content);
if (n > 4096) {
n = 4096;
}
size_t n = strnlen(content, 4096);
memcpy(buf, content, n);
buf[n] = '\0';
return ci_strstr(buf, "apiVersion:") != NULL;
+19 -48
View File
@@ -142,24 +142,15 @@ static void handle_kustomize(cbm_pipeline_ctx_t *ctx, const char *path, const ch
/* ── K8s manifest handler ────────────────────────────────────────── */
/* source/src_len are the already-read file bytes (caller retains ownership and
* must free after this call returns). */
static void handle_k8s_manifest(cbm_pipeline_ctx_t *ctx, const char *path, const char *rel_path,
CBMFileResult *result) {
const char *source, int src_len) {
(void)path; /* retained for symmetry; source is always provided now */
int resource_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_K8S, ctx->project_name, rel_path,
CBM_EXTRACT_BUDGET, NULL, NULL);
free(source);
allocated = true;
}
}
CBMFileResult *res = cbm_extract_file(source, src_len, CBM_LANG_K8S, ctx->project_name,
rel_path, CBM_EXTRACT_BUDGET, NULL, NULL);
if (!res) {
return;
}
@@ -191,9 +182,7 @@ static void handle_k8s_manifest(cbm_pipeline_ctx_t *ctx, const char *path, const
resource_count++;
}
if (allocated) {
cbm_free_result(res);
}
cbm_free_result(res);
cbm_log_info("pass.k8s.manifest", "file", rel_path, "resources", itoa_k8s(resource_count));
}
@@ -226,38 +215,20 @@ int cbm_pipeline_pass_k8s(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
handle_kustomize(ctx, path, rel, cached);
kustomize_count++;
} else if (lang == CBM_LANG_YAML || lang == CBM_LANG_K8S) {
/* Need source content for cbm_is_k8s_manifest check */
if (cached) {
/* Use cached result — the file is a k8s manifest if lang is CBM_LANG_K8S,
* or if we check the source. With a cached result available, trust the
* language field set during discovery. */
if (lang == CBM_LANG_K8S) {
handle_k8s_manifest(ctx, path, rel, cached);
/* 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 (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 */
handle_k8s_manifest(ctx, path, rel, source, src_len);
manifest_count++;
} else {
/* CBM_LANG_YAML: need source to confirm apiVersion presence */
int src_len = 0;
char *source = k8s_read_file(path, &src_len);
if (source) {
if (cbm_is_k8s_manifest(base, source)) {
handle_k8s_manifest(ctx, path, rel, cached);
manifest_count++;
}
free(source);
}
}
} else {
/* No cached result — read source to classify */
int src_len = 0;
char *source = k8s_read_file(path, &src_len);
if (source) {
if (cbm_is_k8s_manifest(base, source)) {
/* Pass NULL result — handle_k8s_manifest will re-extract */
handle_k8s_manifest(ctx, path, rel, NULL);
manifest_count++;
}
free(source);
}
free(source);
}
}
}
+40
View File
@@ -4232,6 +4232,45 @@ TEST(k8s_extract_manifest_no_name) {
PASS();
}
TEST(k8s_extract_manifest_multidoc) {
/* Two-document YAML separated by "---".
* extract_k8s_manifest contains a "break" after the first successful push,
* so it processes only the first document that has both kind and
* metadata.name. This test pins that behaviour: the first document's
* resource must be present and no crash must occur.
*
* Note: with some tree-sitter YAML grammar versions the root stream may
* expose both documents as siblings; the break still fires after the first
* successful def push, so defs.count must be exactly 1. */
const char *src =
"apiVersion: apps/v1\n"
"kind: Deployment\n"
"metadata:\n"
" name: my-app\n"
"---\n"
"apiVersion: v1\n"
"kind: Service\n"
"metadata:\n"
" name: my-svc\n";
CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), CBM_LANG_K8S,
"myproj", "k8s/multi.yaml", 0, NULL, NULL);
ASSERT(r != NULL);
ASSERT(!r->has_error);
/* First document's resource must be present */
int found = 0;
for (int i = 0; i < r->defs.count; i++) {
if (r->defs.items[i].label && strcmp(r->defs.items[i].label, "Resource") == 0 &&
r->defs.items[i].name && strcmp(r->defs.items[i].name, "Deployment/my-app") == 0) {
found = 1;
}
}
ASSERT(found);
/* At least one def, no more than one (only first document processed) */
ASSERT(r->defs.count >= 1);
cbm_free_result(r);
PASS();
}
/* ── Envscan tests (port of envscan_test.go) ───────────────────── */
/* Helper: write a file inside a temp dir */
@@ -5182,6 +5221,7 @@ SUITE(pipeline) {
RUN_TEST(k8s_extract_kustomize);
RUN_TEST(k8s_extract_manifest);
RUN_TEST(k8s_extract_manifest_no_name);
RUN_TEST(k8s_extract_manifest_multidoc);
/* Env URL scanning */
RUN_TEST(envscan_dockerfile_env_urls);
RUN_TEST(envscan_shell_env_urls);