Break-glass merge PR #2644

This commit is contained in:
Martin Vogel authored and GitHub committed 2026-10-14 23:56:23 +02:00
commit 5a6f85faca
12 files changed
+1372 -102

No files matched your search

+3 -2
View File
@@ -333,15 +333,16 @@ The install script placed beside the binary is **reported, not deleted**: uninst
- `INFRA_MAPS`: a Kubernetes Service to each workload whose pod labels carry every `key: value` of the Service's selector, within one namespace
- `CONNECTS_TO`: a Kubernetes workload or compose service to the Service its containers' environment addresses (`PRODUCT_CATALOG_SERVICE_ADDR: productcatalogservice:3550`, a URL's host, `<svc>.<ns>.svc...`, a bare name on an `*_ADDR` / `*_HOST` / `*_URL` / ... key; compose: the project's `.env` interpolated): a Kubernetes Service of the same bundle and namespace, or a compose service of the same file. The edge keeps `via: env`, `key` and `container`, never the value
- `USAGE` with `ref: image`: a Kubernetes workload (any pod template: containers, initContainers, CronJobs, custom resources) or a docker compose service to the `Image` it runs
- `DEPLOYS`: what puts manifests, kustomizations or charts on a cluster, to what it puts there: a Flux Kustomization or Argo Application to the kustomizations / manifests / chart of its path, a Flux HelmRelease to its chart (`spec.chart` or `chartRef` → OCIRepository), a shell command or Make recipe (`kubectl apply -f/-k`, `kustomize build | kubectl apply`, `helm install/upgrade/template`), a Go installer loading a chart with the Helm SDK, a Terraform `helm_release`, a Terraform `kubernetes_*` resource the object it declares (a `Resource` like a manifest's, `ecosystem: terraform`, when its name is a literal), a kustomization inflating a chart (`helmCharts`; its values file an `IMPORTS`). A chart outside the repository is a shared external `Chart`; the edge says `via` (`flux`, `argo`, `helmrelease`, `kubectl`, `kustomize`, `helm`, `helm-sdk`, `terraform`, `helmcharts`, ...)
- `DEPLOYS`: what puts manifests, kustomizations or charts on a cluster, to what it puts there: a Flux Kustomization or Argo Application to the kustomizations / manifests / chart of its path, a Flux HelmRelease to its chart (`spec.chart` or `chartRef` → OCIRepository), a shell command or Make recipe (`kubectl apply -f/-k`, `kustomize build | kubectl apply`, `helm install/upgrade/template`), a Go installer loading a chart with the Helm SDK, a Terraform `helm_release`, a Terraform `kubernetes_*` resource the object it declares (a `Resource` like a manifest's, `ecosystem: terraform`, when its name is a literal), a kustomization inflating a chart (`helmCharts`; its values file an `IMPORTS`). A chart outside the repository is an external `Chart`, keyed by its publisher when its repository URL is stated (`__chart__<repository>/<name>`, the same node in every project, the URL in `repository`), else one per project and name; the edge says `via` (`flux`, `argo`, `helmrelease`, `kubectl`, `kustomize`, `helm`, `helm-sdk`, `terraform`, `helmcharts`, ...)
- `DEPENDS_ON`: a Helm chart to the charts it declares (the local chart for a resolvable `file://` repository, otherwise an external `Chart`; the edge keeps `repository`), and a `go.mod` / `requirements.txt` file to an external `Package`; a Dockerfile to the `Image`s it builds on (`via: from`, `copy-from`); an `Image` to the Dockerfile it is built from (`via: build`; `tool`: `skaffold`, `compose`, `docker-build` for `docker` / `podman` / `nerdctl` / `buildah` build commands in scripts, Make and CI); a compose service to the services its `depends_on` / `links` name
- `CONFIGURES`: code reading an environment variable to the shared `EnvVar` of that name (`strategy: env_access`); a Kubernetes container, compose service or Dockerfile (`ENV` of the image) setting one to that container's own `EnvVar` (`strategy: env_set`, with `source`: `value`, `configMapKeyRef`, `secretKeyRef`, `fieldRef`, ..., `compose`, `dockerfile`; values are never stored); and that container's `EnvVar` to the code reading the name inside the files its image is built from (`strategy: env_read`, with `dockerfile`): the image's files are the sources its Dockerfile's `COPY` / `ADD` put into it, less the context's `.dockerignore`
- `CONFIGURES` with `strategy: key_symbol`: a code symbol to the config key it names (`config_key`). A YAML, JSON, HCL (Terraform, OpenTofu, tfvars), TOML or INI key links by exact name (`maxRetryCount` = `max_retry_count`) and only to code inside the config file's component: its directory, or the one around a `config/`, `configs/`, `conf/`, `settings/` or `etc/` directory. A `.env` or `.properties` key also links by substring (confidence 0.75)
- `OVERRIDE` with `via: patch`: a Kustomize patch to each object of its kustomization's build it changes (child → base): a patch file's documents to the object of the same kind and name; an inline patch (from the kustomization) or a JSON6902 operation file to what its `target` selects (group, version, kind, name, namespace, label selector); a component's in the build of each kustomization including it. The edge keeps `kustomization`, `patch` and `line`; a patch file is an `IMPORTS` (`local_name: patches`) of its kustomization, never a member of the build
- `OVERRIDE` with `via: namePrefix` / `nameSuffix`: an object a kustomization renames, from its variant: a `Resource` named with the deployed name (`capi-` + `controller-manager`; the prefixes of nested kustomizations outermost first, suffixes innermost first) in the namespace that build gives it, `ecosystem: kustomize`, which the kustomization's file `DEFINES`. CRDs, Namespaces and API services keep their names, as kustomize does. The edge keeps `kustomization` and `line`
- `USAGE` with `via: terraform`: a Terraform / OpenTofu reference (`var.X`, `local.X`, `module.X`, `data.T.N`, `T.N`) from the top-level block holding it to the block of its module (the `.tf` / `.tofu` files of its directory) that declares it (a `locals` entry: its `locals` block); an entry of the block's `depends_on` list is a `DEPENDS_ON` instead. The edge keeps `ref` (`var`, `local`, `module`, `data`, `resource`) and `line`; a name only mentioned in string text is no reference. A module call with a local source (`module "net" { source = "./modules/net" }`) `IMPORTS` (`local_name`: the module's name) the `Folder` of that directory (the root directory: the `Project`), and `module.net.id` (also `module.net[0].id`, `module.net[*].id`) `USAGE`s (`ref: output`) the `output "id"` there, beside `module.net`
- `OVERRIDE` with `via: tfvars`: a `.tfvars` assignment to a variable the module of its own directory declares, from the file's `Variable` to that `variable` block (the edge keeps `line`); a `.tfvars` file whose own directory declares none of its names sets the nearest ancestor module's that does, the edge marked `inferred` (such a file is applied by a `-var-file` the repository does not state)
- Terragrunt (`via: terragrunt`): a unit (`terragrunt.hcl`), evaluated in its own context: an `include` `IMPORTS` (`local_name`) the file its `path` names; a `read_terragrunt_config` `IMPORTS` (`read`: the file holding the call) the file it names, from the block holding the call or, for a call in an included file, from the unit's `include` block that brings it in; a `dependency` `DEPENDS_ON` the other unit; a local `terraform` `source` `IMPORTS` its `Folder`. Paths may use `find_in_parent_folders`, `dirname` and `get_terragrunt_dir`; anything else is unknown and links nothing
- Ansible (`via: ansible`): a playbook or task file (a YAML file whose top level is a sequence) `IMPORTS` (`local_name`: the directive) what its directives name: `import_playbook` a playbook, `include_tasks` / `import_tasks` / `include` a task file (from its directory, else its role's `tasks/`), `roles:` items and `include_role` / `import_role` a role's `Folder` (`roles/<name>` at or above its directory). A templated target links nothing. Each task and handler is a `Function` (named by its `name:`, else its module; `source: ansible`, `kind`, `module`) that its file `DEFINES`: in a playbook's plays, a role's `tasks/` and `handlers/` files, and the files those include; a block's tasks, not the block. A `notify` `CALLS` (`via: notify`) each handler of that name or `listen` topic in the task's own role (else its file)
- Ansible (`via: ansible`): a playbook or task file (a YAML file whose top level is a sequence) `IMPORTS` (`local_name`: the directive) what its directives name: `import_playbook` a playbook, `include_tasks` / `import_tasks` / `include` a task file (from its directory, else its role's `tasks/`), `roles:` items and `include_role` / `import_role` a role's `Folder` (`roles/<name>` at or above its directory). A templated target links nothing. Each task and handler is a `Function` (named by its `name:`, else its module; `source: ansible`, `kind`, `module`) that its file `DEFINES`: in a playbook's plays, a role's `tasks/` and `handlers/` files, and the files those include; a block's tasks, not the block. A `notify` `CALLS` (`via: notify`) each handler of that name or `listen` topic in the task's own role (else its file). A `kube` (kubespray's module) or `kubernetes.core.k8s` task `DEPLOYS` (`via: ansible`, `module`) the repository file its play rendered the applied manifest from: the `template` (in the role's `templates/`) or `copy` (its `files/`) task whose `dest` is the task's `filename`, item by item through `register` / `with_items` / `loop` lists the play states (a literal list, a `set_fact`, the role's `defaults` / `vars`), or a `k8s` `template:` argument
- `EMITS`, `LISTENS_ON` (channels)
- `DATA_FLOWS` with arg-to-param mapping + field access chains
- `SIMILAR_TO` (MinHash + LSH near-clone detection, Jaccard scored)
+669 -2
View File
@@ -34,6 +34,28 @@
* directory before its tasks/), else in its file (a play's handlers). The
* incremental repair declines on any change to Ansible content
* (closure_ansible_decline): a notify joins two files.
*
* PHASE 7 P7.3: applies. A kube (kubespray's library module) or k8s
* (kubernetes.core / community.kubernetes) task applies a manifest the play
* rendered: it DEPLOYS {via: ansible, module, line} the repository file the
* render read -- a template task's src (the role's templates/) or a copy
* task's (its files/; never remote_src), else the task file's directory:
* a k8s template: argument that template;
* filename == a renderer's dest (D) that renderer's src;
* {{ item.item.K }} over "{{ R.results }}" (L)
* the src of the renderer registering
* R, for each item of its loop;
* {{ item.K }} (S) for each of the task's own items, the
* src of the renderer of that dest;
* a literal filename (R) the src of each looped renderer item
* whose dest renders to it.
* A loop is a literal list, or "{{ V }}": a set_fact of the file, else the
* role's defaults/ or vars/ main.yml. Templates are searched before copies,
* each in file order. The incremental repair declines on a change to a
* role's defaults / vars main file (closure_ansible_decline), and when a
* task file would join it as a dependent -- a deleted template it applies --
* since the repair would walk it without its role's other files
* (closure_ansible_dependent_decline).
*/
#include "foundation/constants.h"
#include "foundation/compat.h"
@@ -88,6 +110,20 @@ typedef struct {
int note_cap;
int tasks;
int calls;
CBMHashTable *file_nodes; /* P7.3: File nodes by path (what a task applies) */
CBMHashTable *by_rel; /* every input file's path -> its index + 1 */
const cbm_file_info_t *files;
TSParser *parser; /* reads a role's defaults / vars */
struct an_render *renders; /* per file: template / copy tasks */
int render_n;
int render_cap;
struct an_apply *applies; /* per file: kube / k8s tasks */
int apply_n;
int apply_cap;
struct an_fact *facts; /* per file: set_fact lists */
int fact_n;
int fact_cap;
int deploys;
} an_t;
static bool an_type(TSNode n, const char *t) {
@@ -143,6 +179,38 @@ static bool an_scalar(const an_t *a, TSNode n, char *out, size_t sz) {
return out[0] != '\0';
}
/* Is `n` an Ansible boolean that holds true -- y / yes / on / 1 / true / t,
* any case (Ansible's boolean()), a YAML boolean or a string? an_scalar
* reads strings only: `remote_src: true` is a boolean_scalar. */
static bool an_true(const an_t *a, TSNode n) {
static const char *const TRUE_WORDS[] = {"y", "yes", "on", "1", "true", "t"};
while ((an_type(n, "flow_node") || an_type(n, "block_node") || an_type(n, "plain_scalar")) &&
ts_node_named_child_count(n) == SKIP_ONE) {
n = ts_node_named_child(n, 0);
}
char v[CBM_SZ_16];
if (an_type(n, "boolean_scalar") || an_type(n, "integer_scalar")) {
uint32_t b = ts_node_start_byte(n);
uint32_t e = ts_node_end_byte(n);
if (e - b + SKIP_ONE > sizeof(v)) {
return false;
}
memcpy(v, a->src + b, e - b);
v[e - b] = '\0';
} else if (!an_scalar(a, n, v, sizeof(v))) {
return false;
}
for (char *c = v; *c; c++) {
*c = (char)(*c >= 'A' && *c <= 'Z' ? *c - 'A' + 'a' : *c);
}
for (size_t i = 0; i < sizeof(TRUE_WORDS) / sizeof(TRUE_WORDS[0]); i++) {
if (strcmp(v, TRUE_WORDS[i]) == 0) {
return true;
}
}
return false;
}
/* The key of a mapping pair, its text, the ansible.builtin. prefix dropped. */
static bool an_key(const an_t *a, TSNode pair, char *out, size_t sz) {
static const char BUILTIN[] = "ansible.builtin.";
@@ -586,6 +654,586 @@ static bool an_name_at(const an_t *a, TSNode v, TSNode list, uint32_t i, char *o
return an_scalar(a, item, out, sz);
}
/* ── Applies (P7.3) ───────────────────────────────────────────────── */
/* One item of a loop list: a scalar, or a mapping's scalar pairs. */
typedef struct {
char *scalar;
char **keys;
char **vals;
int n;
} an_item_t;
/* A loop (with_items / loop): its literal items, or its expression
* ("{{ X }}", "{{ R.results }}"). */
typedef struct {
an_item_t *items;
int n;
char *expr;
} an_loop_t;
/* A task rendering a file: template (src in the role's templates/) or copy
* of a repository file (its files/). */
typedef struct an_render {
char *src;
char *dest;
char *reg;
an_loop_t loop;
bool copy;
} an_render_t;
/* A task applying a manifest: kube / k8s, its filename (src) or template
* argument, its loop. */
typedef struct an_apply {
int64_t task;
int line;
char *module;
char *fname;
char *tmpl;
an_loop_t loop;
} an_apply_t;
/* A list a set_fact of the file names. */
typedef struct an_fact {
char *name;
an_loop_t list;
} an_fact_t;
static const char *const AN_APPLIERS[] = {"kube", "k8s", "kubernetes.core.k8s",
"community.kubernetes.k8s"};
static void *an_grow(void *items, int *cap, int count, size_t elem) {
if (count < *cap) {
return items;
}
int ncap = *cap ? *cap * PAIR_LEN : AN_GROW_MIN;
void *grown = cbm_realloc(CBM_MEM_CLASS_OTHER, items, (size_t)ncap * elem);
if (grown) {
*cap = ncap;
}
return grown;
}
static char *an_dup(const char *s) {
return s ? cbm_mem_strdup(CBM_MEM_CLASS_OTHER, s) : NULL;
}
/* A value node without its block_node / flow_node wrappers. */
static TSNode an_bare(TSNode v) {
while ((an_type(v, "block_node") || an_type(v, "flow_node")) &&
ts_node_named_child_count(v) == SKIP_ONE) {
v = ts_node_named_child(v, 0);
}
return v;
}
/* A loop item: a scalar, or the scalar pairs of a block / flow mapping. */
static void an_item_parse(const an_t *a, TSNode node, an_item_t *it) {
char buf[CBM_SZ_1K];
*it = (an_item_t){0};
if (an_scalar(a, node, buf, sizeof(buf))) {
it->scalar = an_dup(buf);
return;
}
TSNode m = an_bare(node);
if (!an_type(m, "block_mapping") && !an_type(m, "flow_mapping")) {
return;
}
int cap = 0;
int vcap = 0;
for (uint32_t i = 0; i < ts_node_named_child_count(m); i++) {
TSNode pair = ts_node_named_child(m, i);
char k[CBM_SZ_128];
if (!an_type(pair, "block_mapping_pair") && !an_type(pair, "flow_pair")) {
continue;
}
if (!an_scalar(a, ts_node_child_by_field_name(pair, "key", sizeof("key") - SKIP_ONE), k,
sizeof(k)) ||
!an_scalar(a, ts_node_child_by_field_name(pair, "value", sizeof("value") - SKIP_ONE),
buf, sizeof(buf))) {
continue;
}
char **keys = an_grow(it->keys, &cap, it->n, sizeof(char *));
if (keys) {
it->keys = keys;
}
char **vals = keys ? an_grow(it->vals, &vcap, it->n, sizeof(char *)) : NULL;
if (!keys || !vals) {
return;
}
it->vals = vals;
it->keys[it->n] = an_dup(k);
it->vals[it->n++] = an_dup(buf);
}
}
/* A loop value: an expression (a scalar holding {{ }}, or a list of exactly
* one -- with_items flattens one level), else a literal list of items. */
static void an_loop_parse(const an_t *a, TSNode v, an_loop_t *out) {
char buf[CBM_SZ_1K];
*out = (an_loop_t){0};
if (an_scalar(a, v, buf, sizeof(buf))) {
out->expr = strstr(buf, "{{") ? an_dup(buf) : NULL;
return;
}
TSNode s = an_bare(v);
bool block = an_type(s, "block_sequence");
if (!block && !an_type(s, "flow_sequence")) {
return;
}
int cap = 0;
for (uint32_t i = 0; i < ts_node_named_child_count(s); i++) {
TSNode item = ts_node_named_child(s, i);
if (block ? !an_type(item, "block_sequence_item") : !an_type(item, "flow_node")) {
continue;
}
if (block) {
item = ts_node_named_child(item, 0);
}
an_item_t *grown = an_grow(out->items, &cap, out->n, sizeof(an_item_t));
if (!grown) {
return;
}
out->items = grown;
an_item_parse(a, item, &out->items[out->n++]);
}
if (out->n == SKIP_ONE && out->items[0].scalar && strstr(out->items[0].scalar, "{{")) {
out->expr = out->items[0].scalar;
out->items[0].scalar = NULL;
cbm_free(CBM_MEM_CLASS_OTHER, out->items);
out->items = NULL;
out->n = 0;
}
}
static void an_loop_free(an_loop_t *l) {
for (int i = 0; i < l->n; i++) {
an_item_t *it = &l->items[i];
for (int k = 0; k < it->n; k++) {
cbm_free(CBM_MEM_CLASS_OTHER, it->keys[k]);
cbm_free(CBM_MEM_CLASS_OTHER, it->vals[k]);
}
cbm_free(CBM_MEM_CLASS_OTHER, it->keys);
cbm_free(CBM_MEM_CLASS_OTHER, it->vals);
cbm_free(CBM_MEM_CLASS_OTHER, it->scalar);
}
cbm_free(CBM_MEM_CLASS_OTHER, l->items);
cbm_free(CBM_MEM_CLASS_OTHER, l->expr);
*l = (an_loop_t){0};
}
/* The task's loop: with_items, else loop. */
static void an_task_loop(const an_t *a, TSNode m, an_loop_t *out) {
TSNode v = an_get(a, m, "with_items");
an_loop_parse(a, ts_node_is_null(v) ? an_get(a, m, "loop") : v, out);
}
/* What task `m` (Function `id`) renders, applies or sets, for an_link_applies. */
static void an_collect(an_t *a, TSNode m, int64_t id) {
char k[CBM_SZ_128];
char v[CBM_SZ_1K];
for (uint32_t i = 0; i < ts_node_named_child_count(m); i++) {
TSNode pair = ts_node_named_child(m, i);
if (!an_key(a, pair, k, sizeof(k))) {
continue;
}
TSNode value = ts_node_child_by_field_name(pair, "value", sizeof("value") - SKIP_ONE);
bool copy = strcmp(k, "copy") == 0;
if (strcmp(k, "template") == 0 || copy) {
TSNode args = an_bare(value);
if (!an_type(args, "block_mapping") && !an_type(args, "flow_mapping")) {
continue;
}
if (copy && an_true(a, an_get(a, value, "remote_src"))) {
continue;
}
an_render_t *r = an_grow(a->renders, &a->render_cap, a->render_n, sizeof(*r));
if (!r) {
continue;
}
a->renders = r;
r = &a->renders[a->render_n++];
*r = (an_render_t){.copy = copy};
r->src = an_scalar(a, an_get(a, value, "src"), v, sizeof(v)) ? an_dup(v) : NULL;
r->dest = an_scalar(a, an_get(a, value, "dest"), v, sizeof(v)) ? an_dup(v) : NULL;
r->reg = an_scalar(a, an_get(a, m, "register"), v, sizeof(v)) ? an_dup(v) : NULL;
an_task_loop(a, m, &r->loop);
} else if (strcmp(k, "set_fact") == 0) {
TSNode f = an_bare(value);
for (uint32_t j = 0; an_type(f, "block_mapping") && j < ts_node_named_child_count(f);
j++) {
TSNode fp = ts_node_named_child(f, j);
char name[CBM_SZ_128];
TSNode fv = ts_node_child_by_field_name(fp, "value", sizeof("value") - SKIP_ONE);
TSNode seq = an_bare(fv);
an_fact_t *g =
(an_type(fp, "block_mapping_pair") && an_key(a, fp, name, sizeof(name)) &&
(an_type(seq, "block_sequence") || an_type(seq, "flow_sequence")))
? an_grow(a->facts, &a->fact_cap, a->fact_n, sizeof(*g))
: NULL;
if (g) {
a->facts = g;
a->facts[a->fact_n].name = an_dup(name);
an_loop_parse(a, fv, &a->facts[a->fact_n++].list);
}
}
} else {
for (size_t j = 0; j < sizeof(AN_APPLIERS) / sizeof(AN_APPLIERS[0]); j++) {
if (strcmp(k, AN_APPLIERS[j]) != 0) {
continue;
}
an_apply_t *p = an_grow(a->applies, &a->apply_cap, a->apply_n, sizeof(*p));
if (!p) {
break;
}
a->applies = p;
p = &a->applies[a->apply_n++];
*p = (an_apply_t){.task = id,
.line = (int)ts_node_start_point(m).row + SKIP_ONE,
.module = an_dup(k)};
if (an_scalar(a, an_get(a, value, "template"), v, sizeof(v))) {
p->tmpl = an_dup(v);
} else if (an_scalar(a, an_get(a, value, "filename"), v, sizeof(v)) ||
an_scalar(a, an_get(a, value, "src"), v, sizeof(v))) {
p->fname = an_dup(v);
}
an_task_loop(a, m, &p->loop);
break;
}
}
}
}
/* The trimmed inside of a {{ }}, `n` bytes at `b`: "item" (key length 0) or
* "item.K", K an identifier (into key / kl); false for anything else. */
static bool an_item_ref(const char *b, size_t n, const char **key, size_t *kl) {
static const char ITEM[] = "item";
size_t il = sizeof(ITEM) - SKIP_ONE;
if (n < il || strncmp(b, ITEM, il) != 0) {
return false;
}
*key = b + il;
*kl = 0;
if (n == il) {
return true;
}
if (b[il] != '.' || n == il + SKIP_ONE) {
return false;
}
*key = b + il + SKIP_ONE;
*kl = n - il - SKIP_ONE;
for (size_t i = 0; i < *kl; i++) {
char c = (*key)[i];
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' ||
(i > 0 && c >= '0' && c <= '9'))) {
return false;
}
}
return true;
}
/* `pattern` with each {{ item }} / {{ item.K }} replaced from `it`; false
* when one cannot be, or -- `strict` -- when any other {{ }} is left. */
static bool an_subst(const char *pattern, const an_item_t *it, bool strict, char *out, size_t sz) {
size_t w = 0;
for (const char *p = pattern; p && *p;) {
const char *end = p[0] == '{' && p[SKIP_ONE] == '{' ? strstr(p + PAIR_LEN, "}}") : NULL;
const char *piece = p;
size_t len = SKIP_ONE;
if (end) {
const char *b = p + PAIR_LEN;
while (*b == ' ' || *b == '\t') {
b++;
}
size_t n = (size_t)(end - b); /* b stops at end's '}' at the latest */
while (n > 0 && (b[n - SKIP_ONE] == ' ' || b[n - SKIP_ONE] == '\t')) {
n--;
}
const char *key = NULL;
size_t kl = 0;
if (an_item_ref(b, n, &key, &kl)) {
piece = kl == 0 ? it->scalar : NULL;
for (int k = 0; kl > 0 && k < it->n && !piece; k++) {
if (strlen(it->keys[k]) == kl && strncmp(it->keys[k], key, kl) == 0) {
piece = it->vals[k];
}
}
if (!piece) {
return false;
}
len = strlen(piece);
} else if (strict) {
return false;
} else {
len = (size_t)(end + PAIR_LEN - p);
}
p = end + PAIR_LEN;
} else {
p++;
}
if (w + len >= sz) {
return false;
}
memcpy(out + w, piece, len);
w += len;
}
out[w] = '\0';
return true;
}
/* The repository file a template / copy `src` names: in the role's
* templates/ (files/ for a copy), else the task file's directory. */
static const cbm_gbuf_node_t *an_src_file(const an_t *a, const char *src, bool copy) {
char base[CBM_SZ_1K];
char path[CBM_SZ_1K];
if (!src || strstr(src, "{{")) {
return NULL;
}
if (a->role_dir[0]) {
snprintf(base, sizeof(base), "%s/%s", a->role_dir, copy ? "files" : "templates");
const cbm_gbuf_node_t *n = cbm_repo_rel_join(base, src, path, sizeof(path))
? cbm_ht_get(a->file_nodes, path)
: NULL;
if (n) {
return n;
}
}
return cbm_repo_rel_join(a->dir, src, path, sizeof(path)) ? cbm_ht_get(a->file_nodes, path)
: NULL;
}
/* A top-level list `name` of the role's defaults/ or vars/ main.yml, into
* `out`; false when none holds one. */
static bool an_role_list(an_t *a, const char *name, an_loop_t *out) {
static const char *const FILES[] = {"defaults/main.yml", "defaults/main.yaml", "vars/main.yml",
"vars/main.yaml"};
bool found = false;
for (size_t f = 0; a->role_dir[0] && !found && f < sizeof(FILES) / sizeof(FILES[0]); f++) {
char rel[CBM_SZ_1K];
snprintf(rel, sizeof(rel), "%s/%s", a->role_dir, FILES[f]);
intptr_t ix = (intptr_t)cbm_ht_get(a->by_rel, rel);
size_t len = 0;
char *src = ix > 0 ? an_read(a->files[ix - SKIP_ONE].path, &len) : NULL;
TSTree *tree = src ? ts_parser_parse_string(a->parser, NULL, src, (uint32_t)len) : NULL;
TSNode top =
tree ? an_bare(an_child(an_child(ts_tree_root_node(tree), "document"), "block_node"))
: (TSNode){0};
const char *saved = a->src;
a->src = src;
for (uint32_t i = 0;
an_type(top, "block_mapping") && !found && i < ts_node_named_child_count(top); i++) {
TSNode pair = ts_node_named_child(top, i);
char k[CBM_SZ_128];
TSNode v = ts_node_child_by_field_name(pair, "value", sizeof("value") - SKIP_ONE);
TSNode seq = an_bare(v);
if (an_key(a, pair, k, sizeof(k)) && strcmp(k, name) == 0 &&
(an_type(seq, "block_sequence") || an_type(seq, "flow_sequence"))) {
an_loop_parse(a, v, out);
found = true;
}
}
a->src = saved;
if (tree) {
ts_tree_delete(tree);
}
cbm_free(CBM_MEM_CLASS_OTHER, src);
}
return found;
}
/* An expression "{{ X }}" with X an identifier, then `tail` ("" or
* ".results"): X into `out`. */
static bool an_expr_name(const char *expr, const char *tail, char *out, size_t sz) {
const char *b = expr && expr[0] == '{' && expr[SKIP_ONE] == '{' ? expr + PAIR_LEN : NULL;
size_t n = 0;
while (b && (*b == ' ' || *b == '\t')) {
b++;
}
while (b && n + SKIP_ONE < sz &&
((*b >= 'a' && *b <= 'z') || (*b >= 'A' && *b <= 'Z') || *b == '_' ||
(n > 0 && *b >= '0' && *b <= '9'))) {
out[n++] = *b++;
}
out[n] = '\0';
size_t tl = strlen(tail);
if (!n || !b || strncmp(b, tail, tl) != 0) {
return false;
}
for (b += tl; *b == ' ' || *b == '\t'; b++) {}
return strcmp(b, "}}") == 0;
}
/* "{{ R.results }}": R into `out`. */
static bool an_results_of(const char *expr, char *out, size_t sz) {
return an_expr_name(expr, ".results", out, sz);
}
/* Does `s` hold a {{ item.K }} reference (one key level)? */
static bool an_has_item_key(const char *s) {
for (const char *p = s ? strstr(s, "{{") : NULL; p; p = strstr(p + PAIR_LEN, "{{")) {
const char *b = p + PAIR_LEN;
while (*b == ' ' || *b == '\t') {
b++;
}
if (strncmp(b, "item.", sizeof("item.") - SKIP_ONE) != 0) {
continue;
}
b += sizeof("item.") - SKIP_ONE;
const char *k = b;
while ((*b >= 'a' && *b <= 'z') || (*b >= 'A' && *b <= 'Z') || *b == '_') {
b++;
}
while (b > k && (*b == ' ' || *b == '\t')) {
b++;
}
if (b > k && b[0] == '}' && b[SKIP_ONE] == '}') {
return true;
}
}
return false;
}
/* The items a loop goes over: its literal list, or the list "{{ V }}" names
* -- a set_fact of the file, else the role's defaults / vars. `scratch`
* holds a list read from the role (the caller frees it). */
static const an_loop_t *an_items(an_t *a, const an_loop_t *l, an_loop_t *scratch) {
if (!l->expr) {
return l->items ? l : NULL;
}
char name[CBM_SZ_128];
if (!an_expr_name(l->expr, "", name, sizeof(name))) {
return NULL;
}
for (int i = 0; i < a->fact_n; i++) {
if (strcmp(a->facts[i].name, name) == 0) {
return &a->facts[i].list;
}
}
return an_role_list(a, name, scratch) ? scratch : NULL;
}
static void an_deploy(an_t *a, const an_apply_t *p, const cbm_gbuf_node_t *target) {
if (!target || p->task <= 0) {
return;
}
char em[CBM_SZ_256];
char props[CBM_SZ_512];
cbm_json_escape(em, (int)sizeof(em), p->module);
snprintf(props, sizeof(props), "{\"via\":\"ansible\",\"module\":\"%s\",\"line\":%d}", em,
p->line);
cbm_gbuf_insert_edge(a->ctx->gbuf, p->task, target->id, "DEPLOYS", props);
a->deploys++;
}
/* The renderer `src` of each item of `items`, substituted (strict). */
static int an_deploy_items(an_t *a, const an_apply_t *p, const an_render_t *r,
const an_loop_t *items) {
int made = 0;
char path[CBM_SZ_1K];
for (int i = 0; r && items && i < items->n; i++) {
const cbm_gbuf_node_t *t =
r->src && an_subst(r->src, &items->items[i], true, path, sizeof(path))
? an_src_file(a, path, r->copy)
: NULL;
if (t) {
an_deploy(a, p, t);
made++;
}
}
return made;
}
/* The first renderer (templates before copies, each in file order) whose
* `dest` -- or, with `reg`, whose register -- is the very string. */
static const an_render_t *an_render_find(const an_t *a, const char *dest, const char *reg) {
for (int pass = 0; pass < PAIR_LEN; pass++) {
for (int i = 0; i < a->render_n; i++) {
const an_render_t *r = &a->renders[i];
const char *v = reg ? r->reg : r->dest;
if (r->copy == (pass == SKIP_ONE) && v && strcmp(v, reg ? reg : dest) == 0) {
return r;
}
}
}
return NULL;
}
/* kube / k8s -> DEPLOYS {via: ansible, module, line} the repository file it
* applies, traced to the template (or copied file) the play rendered it from:
* a k8s template: argument; a renderer whose dest is the filename (D); for
* {{ item.item.K }} over "{{ R.results }}", each item of the renderer
* registering R (L); for {{ item.K }}, each of the task's own items through
* the renderer of that dest (S); a literal filename a looped renderer's dest
* renders to (R). */
static void an_link_applies(an_t *a) {
for (int i = 0; i < a->apply_n; i++) {
const an_apply_t *p = &a->applies[i];
an_loop_t scratch = {0};
int made = 0;
if (p->tmpl) {
an_deploy(a, p, an_src_file(a, p->tmpl, false));
continue;
}
if (!p->fname) {
continue;
}
const an_render_t *r = an_render_find(a, p->fname, NULL);
const cbm_gbuf_node_t *d = r ? an_src_file(a, r->src, r->copy) : NULL;
if (d) {
an_deploy(a, p, d);
continue;
}
char reg[CBM_SZ_128];
if (strstr(p->fname, "item.item") && an_results_of(p->loop.expr, reg, sizeof(reg))) {
const an_render_t *g = an_render_find(a, NULL, reg);
made = g ? an_deploy_items(a, p, g, an_items(a, &g->loop, &scratch)) : 0;
}
if (!made && an_has_item_key(p->fname) && (p->loop.expr || p->loop.items)) {
an_loop_free(&scratch);
made = an_deploy_items(a, p, r, an_items(a, &p->loop, &scratch));
}
if (!made && !strstr(p->fname, "{{ item")) {
char path[CBM_SZ_1K];
for (int j = 0; j < a->render_n; j++) {
const an_render_t *x = &a->renders[j];
an_loop_free(&scratch);
const an_loop_t *items = an_items(a, &x->loop, &scratch);
for (int k = 0; items && x->dest && k < items->n; k++) {
if (an_subst(x->dest, &items->items[k], false, path, sizeof(path)) &&
strcmp(path, p->fname) == 0 && x->src &&
an_subst(x->src, &items->items[k], true, path, sizeof(path))) {
an_deploy(a, p, an_src_file(a, path, x->copy));
}
}
}
}
an_loop_free(&scratch);
}
}
/* The per-file render / apply / set_fact records, freed. */
static void an_applies_reset(an_t *a) {
for (int i = 0; i < a->render_n; i++) {
cbm_free(CBM_MEM_CLASS_OTHER, a->renders[i].src);
cbm_free(CBM_MEM_CLASS_OTHER, a->renders[i].dest);
cbm_free(CBM_MEM_CLASS_OTHER, a->renders[i].reg);
an_loop_free(&a->renders[i].loop);
}
for (int i = 0; i < a->apply_n; i++) {
cbm_free(CBM_MEM_CLASS_OTHER, a->applies[i].module);
cbm_free(CBM_MEM_CLASS_OTHER, a->applies[i].fname);
cbm_free(CBM_MEM_CLASS_OTHER, a->applies[i].tmpl);
an_loop_free(&a->applies[i].loop);
}
for (int i = 0; i < a->fact_n; i++) {
cbm_free(CBM_MEM_CLASS_OTHER, a->facts[i].name);
an_loop_free(&a->facts[i].list);
}
a->render_n = 0;
a->apply_n = 0;
a->fact_n = 0;
}
static void an_index_handler(an_t *a, const char *name, int64_t id) {
char key[CBM_SZ_2K];
if (a->role_dir[0]) {
@@ -654,6 +1302,7 @@ static void an_task(an_t *a, TSNode m, bool handler) {
}
cbm_gbuf_insert_edge(a->ctx->gbuf, a->from, id, "DEFINES", "{}");
a->tasks++;
an_collect(a, m, id);
TSNode list;
char n[CBM_SZ_256];
if (handler) {
@@ -926,6 +1575,13 @@ static void an_free(an_t *a) {
cbm_ht_foreach(a->handlers, an_free_ids_entry, NULL);
cbm_ht_free(a->handlers);
}
an_applies_reset(a);
cbm_free(CBM_MEM_CLASS_OTHER, a->renders);
cbm_free(CBM_MEM_CLASS_OTHER, a->applies);
cbm_free(CBM_MEM_CLASS_OTHER, a->facts);
if (a->by_rel) {
cbm_ht_free(a->by_rel);
}
}
/* Parse `files[i]` when it is a YAML file whose top level is a sequence;
@@ -974,9 +1630,16 @@ void cbm_pipeline_pass_ansible(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *f
}
cbm_free(CBM_MEM_CLASS_OTHER, src);
}
/* pass 2: the tasks of the Ansible files, then their notifies */
/* pass 2: the tasks of the Ansible files (what each applies), then their notifies */
an_mark_ansible(&a, &cands, files);
a.handlers = cands.count > 0 ? cbm_ht_create(0) : NULL;
a.by_rel = a.handlers ? cbm_ht_create(0) : NULL;
for (int i = 0; a.by_rel && i < file_count; i++) {
cbm_ht_set(a.by_rel, files[i].rel_path, (void *)(intptr_t)(i + SKIP_ONE));
}
a.files = files;
a.file_nodes = file_nodes;
a.parser = parser;
for (int c = 0; a.handlers && c < cands.count; c++) {
const cbm_file_info_t *f = &files[cands.items[c].file];
char *src = NULL;
@@ -985,6 +1648,8 @@ void cbm_pipeline_pass_ansible(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *f
if (fnode) {
an_begin_file(&a, f->rel_path, fnode, src);
an_file_tasks(&a, an_top(ts_tree_root_node(tree)), cands.items[c].playbook);
an_link_applies(&a);
an_applies_reset(&a);
}
if (tree) {
ts_tree_delete(tree);
@@ -1004,9 +1669,11 @@ void cbm_pipeline_pass_ansible(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *f
char e[CBM_SZ_32];
char t[CBM_SZ_32];
char n[CBM_SZ_32];
char d[CBM_SZ_32];
snprintf(e, sizeof(e), "%d", a.edges);
snprintf(t, sizeof(t), "%d", a.tasks);
snprintf(n, sizeof(n), "%d", a.calls);
cbm_log_info("pass.ansible", "imports", e, "tasks", t, "notifies", n);
snprintf(d, sizeof(d), "%d", a.deploys);
cbm_log_info("pass.ansible", "imports", e, "tasks", t, "notifies", n, "deploys", d);
}
}
+11 -8
View File
@@ -116,15 +116,18 @@ static const cbm_gbuf_node_t **canonical_node_copy(const cbm_gbuf_node_t *const
return sorted;
}
/* Infrastructure files: YAML, JSON and HCL (Terraform, OpenTofu, tfvars). A
* key_symbol pair with either side in one links by exact name and only inside
* the config file's component (user decision 2026-10-10): in 17 infra
* repositories 4 of 40 sampled key_symbol edges were right, all of them exact
* and within one directory tree; the rest were substring and cross-component
* coincidences. Other config formats keep exact + substring matching. */
/* Files whose key_symbol pairs link by exact name and only inside the config
* file's component: infrastructure files -- YAML, JSON, HCL (Terraform,
* OpenTofu, tfvars) -- (user decision 2026-10-10: in 17 infra repositories 4
* of 40 sampled key_symbol edges were right, all of them exact and within one
* directory tree; the rest were substring and cross-component coincidences),
* and TOML and INI files (user decision 2026-10-14: all 72 of their substring
* or cross-component edges in those repositories were coincidences, such as
* `.gitleaks.toml useDefault` <- TestConfig_UseDefault). Other config formats
* (.env, .properties) keep exact + substring matching. */
static bool cl_is_infra_path(const char *path) {
static const char *const exts[] = {".yaml", ".yml", ".json", ".tf",
".hcl", ".tofu", ".tfvars", NULL};
static const char *const exts[] = {".yaml", ".yml", ".json", ".tf", ".hcl", ".tofu",
".tfvars", ".toml", ".ini", ".cfg", ".conf", NULL};
const char *dot = path ? strrchr(path, '.') : NULL;
const char *slash = path ? strrchr(path, '/') : NULL;
if (!dot || (slash && dot < slash)) {
+177 -27
View File
@@ -26,6 +26,7 @@
#include "foundation/str_util.h" /* cbm_json_escape */
#include "pipeline/worker_pool.h"
#include <ctype.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <string.h>
@@ -2542,14 +2543,159 @@ static char *k8s_dep_props(const char *repository) {
return props;
}
/* DEPENDS_ON from `chart_id` to the shared external Chart node of `name`
* (one per name and project, so charts depending on the same chart meet). */
/* ── External charts ─────────────────────────────────────────────── */
/* An external Chart is keyed by its publisher: `__chart__<url>/<name>`, the
* same QN in every project, so two repositories naming one published chart
* name one node. A chart whose repository does not resolve to a URL (a
* `@alias`, an empty or templated repository, a source the pass cannot read)
* keeps the per-project `<project>.__helm_dep__.<name>`: no identity is
* claimed for what was not stated. */
static const char K8S_CHART_QN_PREFIX[] = "__chart__";
static const char K8S_HELM_DEP_MARK[] = ".__helm_dep__.";
/* A byte a stated repository URL may hold: no whitespace or control byte,
* no quote, template or shell expansion, escape or angle bracket. */
static bool k8s_url_byte_ok(unsigned char c) {
return c > ' ' && c < 0x7f && !strchr("\"'`{}$<>\\", c);
}
bool cbm_chart_repo_url(const char *url, char *out, size_t sz) {
if (!url || !out || sz == 0) {
return false;
}
while (*url == ' ' || *url == '\t') {
url++;
}
size_t n = strlen(url);
while (n > 0 && isspace((unsigned char)url[n - SKIP_ONE])) {
n--;
}
if (n == 0 || n + SKIP_ONE > sz || !isalpha((unsigned char)url[0])) {
return false;
}
for (size_t i = 0; i < n; i++) {
if (!k8s_url_byte_ok((unsigned char)url[i])) {
return false;
}
}
/* a query or fragment may carry a token (?token=...): like credentials
* in the authority, never put into a QN or a property */
if (memchr(url, '?', n) || memchr(url, '#', n)) {
return false;
}
size_t s = 0; /* the scheme: [A-Za-z][A-Za-z0-9+.-]* then :// */
while (s < n &&
(isalnum((unsigned char)url[s]) || url[s] == '+' || url[s] == '-' || url[s] == '.')) {
s++;
}
if (s + K8S_SLEN("://") > n || strncmp(url + s, "://", K8S_SLEN("://")) != 0) {
return false;
}
size_t host = s + K8S_SLEN("://");
size_t path = host;
while (path < n && url[path] != '/') {
path++;
}
/* no host, or credentials in the authority: never put into a QN */
if (path == host || memchr(url + host, '@', path - host)) {
return false;
}
for (size_t i = 0; i < path; i++) {
out[i] = (char)tolower((unsigned char)url[i]);
}
memcpy(out + path, url + path, n - path);
out[n] = '\0';
if (strncmp(out, "file://", K8S_SLEN("file://")) == 0) {
return false; /* a directory, not a publisher */
}
size_t end = n;
while (end > path && out[end - SKIP_ONE] == '/') {
end--;
}
if (end - path >= K8S_SLEN(".git") &&
strncmp(out + end - K8S_SLEN(".git"), ".git", K8S_SLEN(".git")) == 0) {
end -= K8S_SLEN(".git");
while (end > path && out[end - SKIP_ONE] == '/') {
end--;
}
}
out[end] = '\0';
return true;
}
bool cbm_chart_ref_repo(const char *ref, char *out, size_t sz) {
if (!ref || !out || sz == 0) {
return false;
}
size_t end = strlen(ref);
while (end > 0 && ref[end - SKIP_ONE] == '/') {
end--;
}
while (end > 0 && ref[end - SKIP_ONE] != '/') {
end--;
}
while (end > 0 && ref[end - SKIP_ONE] == '/') {
end--;
}
if (end == 0 || end + SKIP_ONE > sz) {
return false;
}
memcpy(out, ref, end);
out[end] = '\0';
return true;
}
/* QN and node properties of the external Chart `name` (non-NULL) of
* repository `url` (NULL: unknown). */
static void k8s_chart_identity(const char *project, const char *name, const char *url, char *qn,
size_t qsz, char *props, size_t psz) {
char norm[CBM_SZ_1K];
if (name[0] && !strchr(name, '/') && cbm_chart_repo_url(url, norm, sizeof(norm))) {
int w = snprintf(qn, qsz, "%s%s/%s", K8S_CHART_QN_PREFIX, norm, name);
if (w > 0 && (size_t)w < qsz) {
char esc[CBM_SZ_2K];
cbm_json_escape(esc, (int)sizeof(esc), norm);
snprintf(props, psz, "{\"source\":\"helm\",\"external\":true,\"repository\":\"%s\"}",
esc);
return;
}
}
snprintf(qn, qsz, "%s%s%s", project, K8S_HELM_DEP_MARK, name);
snprintf(props, psz, "{\"source\":\"helm\",\"external\":true}");
}
int64_t cbm_pipeline_external_chart(cbm_pipeline_ctx_t *ctx, const char *name, const char *url,
const char *file) {
if (!name) {
return 0;
}
char qn[CBM_SZ_2K];
char props[CBM_SZ_4K];
k8s_chart_identity(ctx->project_name, name, url, qn, sizeof(qn), props, sizeof(props));
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, props);
}
bool cbm_pipeline_chart_is_external(const cbm_gbuf_node_t *node) {
const char *qn = node ? node->qualified_name : NULL;
return qn && (strncmp(qn, K8S_CHART_QN_PREFIX, K8S_SLEN(K8S_CHART_QN_PREFIX)) == 0 ||
strstr(qn, K8S_HELM_DEP_MARK) != NULL);
}
/* DEPENDS_ON from `chart_id` to the external Chart node of `name` in
* `repository` (charts depending on the same published chart meet). */
static bool k8s_external_dep(cbm_pipeline_ctx_t *ctx, int64_t chart_id, const char *name,
const char *chart_rel, const char *props) {
char dep_qn[CBM_SZ_512];
snprintf(dep_qn, sizeof(dep_qn), "%s.__helm_dep__.%s", ctx->project_name, name);
int64_t dep_id = cbm_gbuf_upsert_node(ctx->gbuf, "Chart", name, dep_qn, chart_rel, SKIP_ONE, 0,
"{\"source\":\"helm\",\"external\":true}");
const char *repository, const char *chart_rel, const char *props) {
char dep_qn[CBM_SZ_2K];
char node_props[CBM_SZ_4K];
k8s_chart_identity(ctx->project_name, name, repository, dep_qn, sizeof(dep_qn), node_props,
sizeof(node_props));
int64_t dep_id =
cbm_gbuf_upsert_node(ctx->gbuf, "Chart", name, dep_qn, chart_rel, SKIP_ONE, 0, node_props);
if (dep_id <= 0) {
return false;
}
@@ -2613,7 +2759,9 @@ static void k8s_link_local_deps(cbm_pipeline_ctx_t *ctx, k8s_local_deps_t *deps)
target->id != d->chart_id) {
cbm_gbuf_insert_edge(ctx->gbuf, d->chart_id, target->id, "DEPENDS_ON", d->props);
} else if (d->chart_rel) {
(void)k8s_external_dep(ctx, d->chart_id, d->name, d->chart_rel, d->props);
/* a file:// directory this repository cannot resolve names no
* publisher: the per-project external Chart */
(void)k8s_external_dep(ctx, d->chart_id, d->name, NULL, d->chart_rel, d->props);
}
}
k8s_local_deps_free(deps);
@@ -2621,8 +2769,9 @@ static void k8s_link_local_deps(cbm_pipeline_ctx_t *ctx, k8s_local_deps_t *deps)
/* Emit a Chart node for a Chart.yaml and a DEPENDS_ON edge per declared
* dependency (#338): to the chart in this repository for a resolvable
* `file://` dependency (once all charts are read), otherwise to a shared
* external Chart node per dependency name, the repository on the edge. */
* `file://` dependency (once all charts are read), otherwise to the external
* Chart node of the dependency's repository and name, the repository also on
* the edge. */
static void handle_helm_chart(cbm_pipeline_ctx_t *ctx, const char *rel_path, const char *source,
k8s_local_deps_t *local_deps) {
cbm_helm_chart_t hc;
@@ -2656,7 +2805,7 @@ static void handle_helm_chart(cbm_pipeline_ctx_t *ctx, const char *rel_path, con
continue;
}
char *props = dep->repository[0] ? k8s_dep_props(dep->repository) : NULL;
dep_edges += k8s_external_dep(ctx, chart_id, dep->name, rel_path, props);
dep_edges += k8s_external_dep(ctx, chart_id, dep->name, dep->repository, rel_path, props);
cbm_free(CBM_MEM_CLASS_OTHER, props);
}
cbm_helm_chart_free(&hc);
@@ -3439,7 +3588,7 @@ static void k8s_link_includes(cbm_pipeline_ctx_t *ctx, k8s_bundles_t *b,
* outermost kustomizations below it -- their Kustomization node -- and the
* manifests outside any, their File node; a chart dir: its Chart); a
* HelmRelease -> its chart (a repository chart dir of a Git/Bucket source,
* else the shared external Chart of that name). */
* else the external Chart of that name). */
typedef struct {
cbm_pipeline_ctx_t *ctx;
@@ -3475,21 +3624,17 @@ static int64_t k8s_chart_node(k8s_dep_link_t *l, const char *dir) {
return 0;
}
/* The shared external Chart of `name` (the one chart dependencies use). */
static int64_t k8s_external_chart(k8s_dep_link_t *l, const char *name, const char *file) {
char qn[CBM_SZ_512];
snprintf(qn, sizeof(qn), "%s.__helm_dep__.%s", l->ctx->project_name, name);
const cbm_gbuf_node_t *known = cbm_gbuf_find_by_qn(l->ctx->gbuf, qn);
if (known) {
return known->id;
}
return cbm_gbuf_upsert_node(l->ctx->gbuf, "Chart", name, qn, file, SKIP_ONE, 0,
"{\"source\":\"helm\",\"external\":true}");
/* The external Chart of `name` in repository `url` (NULL: unknown), the one
* chart dependencies use. */
static int64_t k8s_external_chart(k8s_dep_link_t *l, const char *name, const char *url,
const char *file) {
return cbm_pipeline_external_chart(l->ctx, name, url, file);
}
/* A HelmRelease whose chartRef names OCIRepository `oci` (its USAGE edge
* {"ref":"chartRef"}, resolved with the references) deploys the chart that
* repository fetches: the shared external Chart of the URL's last segment. */
* repository fetches: the external Chart of that OCI reference (its last
* segment the chart, the rest its repository). */
static void k8s_link_chart_refs(k8s_dep_link_t *l, const k8s_hr_t *oci) {
const char *url = oci->chart;
size_t end = strlen(url);
@@ -3513,6 +3658,8 @@ static void k8s_link_chart_refs(k8s_dep_link_t *l, const k8s_hr_t *oci) {
snprintf(props, sizeof(props),
"{\"via\":\"helmrelease\",\"chart\":\"%s\",\"external\":true,\"url\":\"%s\"}",
esc_name, esc_url);
char repo[CBM_SZ_1K];
const char *repo_url = cbm_chart_ref_repo(url, repo, sizeof(repo)) ? repo : NULL;
const cbm_gbuf_edge_t **in = NULL;
int n = 0;
if (cbm_gbuf_find_edges_by_target_type(l->ctx->gbuf, oci->node, "USAGE", &in, &n) != 0) {
@@ -3523,7 +3670,8 @@ static void k8s_link_chart_refs(k8s_dep_link_t *l, const k8s_hr_t *oci) {
const char *p = in[i]->properties_json;
const cbm_gbuf_node_t *hr = cbm_gbuf_find_by_id(l->ctx->gbuf, in[i]->source_id);
if (p && strstr(p, "\"ref\":\"chartRef\"") && hr && hr->file_path) {
k8s_deploys_edge(l, hr->id, k8s_external_chart(l, name, hr->file_path), props);
k8s_deploys_edge(l, hr->id, k8s_external_chart(l, name, repo_url, hr->file_path),
props);
}
}
}
@@ -3542,7 +3690,7 @@ static void k8s_prop_add(char *out, size_t out_sz, int *w, const char *key, cons
* build: DEPLOYS {via: helmcharts, external, chart, url, version, release,
* namespace, values, line} from the kustomization (its Kustomization
* Resource, else its File -- the unit a Flux / Argo deploy reaches) to the
* shared external Chart of that name. */
* external Chart of that repository and name. */
static void k8s_link_kcharts(k8s_dep_link_t *l, const k8s_facts_t *f) {
for (int i = 0; i < f->kchart_n; i++) {
const k8s_kchart_t *c = &f->kcharts[i];
@@ -3563,7 +3711,7 @@ static void k8s_link_kcharts(k8s_dep_link_t *l, const k8s_facts_t *f) {
continue;
}
snprintf(props + w, sizeof(props) - (size_t)w, ",\"line\":%d}", c->line);
k8s_deploys_edge(l, src, k8s_external_chart(l, c->chart, c->kust), props);
k8s_deploys_edge(l, src, k8s_external_chart(l, c->chart, c->url, c->kust), props);
}
}
@@ -4230,7 +4378,9 @@ static void k8s_link_deploys(cbm_pipeline_ctx_t *ctx, k8s_kusts_t *ks, k8s_bundl
k8s_deploys_edge(&l, hr->node, k8s_chart_node(&l, dir), props);
}
} else if (src && src->file_path && !strchr(hr->chart, '/') && !strchr(hr->chart, '$')) {
k8s_deploys_edge(&l, hr->node, k8s_external_chart(&l, hr->chart, src->file_path),
/* the source's URL is not read (a HelmRepository's spec.url):
* the per-project external Chart */
k8s_deploys_edge(&l, hr->node, k8s_external_chart(&l, hr->chart, NULL, src->file_path),
props);
}
}
+1 -1
View File
@@ -1640,7 +1640,7 @@ static CBMHashTable *gc_chart_index(const cbm_gbuf_t *gb) {
const char *qn = nodes[i]->qualified_name;
const char *slash = fp ? strrchr(fp, '/') : NULL;
const char *base = slash ? slash + SKIP_ONE : fp;
if (!fp || !qn || strstr(qn, ".__helm_dep__.") ||
if (!fp || !qn || cbm_pipeline_chart_is_external(nodes[i]) ||
(strcmp(base, "Chart.yaml") != 0 && strcmp(base, "Chart.yml") != 0) || !slash) {
continue;
}
+7 -12
View File
@@ -11,8 +11,8 @@
* kubectl ... -k D the Kustomization of D
* kustomize build D | ... | kubectl apply -f - (also kubectl kustomize D)
* helm install|upgrade [NAME] CHART the Chart of a chart dir, else the
* shared external Chart (repo/name,
* oci://.../name)
* external Chart of that name
* (repo/name, oci://.../name)
* helm template [NAME] CHART | ... | kubectl apply -f -
* find D -name G -exec kubectl apply -f {} \; D's manifests matching G
*
@@ -216,7 +216,7 @@ static bool sh_index_build(sh_index_t *ix, cbm_pipeline_ctx_t *ctx) {
const char *base = fp ? sh_basename(fp) : "";
const char *qn = nodes[i]->qualified_name;
if ((strcmp(base, "Chart.yaml") == 0 || strcmp(base, "Chart.yml") == 0) && qn &&
!strstr(qn, ".__helm_dep__.")) {
!cbm_pipeline_chart_is_external(nodes[i])) {
cbm_ht_set(ix->charts, fp, (void *)(intptr_t)nodes[i]->id);
}
}
@@ -3569,16 +3569,11 @@ static int64_t sh_source(cbm_pipeline_ctx_t *ctx, const sh_span_t *spans, int n,
return fn ? fn->id : 0;
}
/* The shared external Chart of `name` (the one chart dependencies use). */
/* The external Chart of `name` a helm command names: the hit keeps no
* repository URL (a `repo/name` alias, an oci:// reference's last segment),
* so the per-project one. */
static int64_t sh_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}");
return cbm_pipeline_external_chart(ctx, name, NULL, file);
}
/* A hit's CI context as JSON members (`,"ci":..,"job":..`); "" for none. */
+14 -16
View File
@@ -7,8 +7,9 @@
* 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
* repository = "<url>", chart = "<name>" the external Chart of
* chart = "oci://registry/.../<name>" that name (an oci://
* reference: of its URL)
* 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.
@@ -198,7 +199,9 @@ static CBMHashTable *tf_chart_index(cbm_pipeline_ctx_t *ctx) {
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__.") &&
/* an external chart a Chart.yaml depends on carries that file's
* path too: only the chart it declares is the directory's */
if (fp && qn && !cbm_pipeline_chart_is_external(nodes[i]) &&
(strcmp(base, "Chart.yaml") == 0 || strcmp(base, "Chart.yml") == 0)) {
cbm_ht_set(ix, fp, (void *)(intptr_t)nodes[i]->id);
}
@@ -220,18 +223,6 @@ static int64_t tf_local_chart(const tf_t *t, const char *dir) {
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, by the QN
* its definition got -- not among every block of that name: modules that
* share a release name (`this`) would make each lookup scan all of them. */
@@ -293,6 +284,9 @@ static void tf_helm_release(tf_t *t, TSNode block, const char *name) {
int64_t src = tf_block_node(t, name);
int64_t tgt = 0;
bool external = false;
/* the repository of an oci:// reference; "" when unknown (a
* `repository =` value is not read) */
char repo[CBM_SZ_1K] = "";
char path[CBM_SZ_1K] = "";
static const char OCI[] = "oci://";
if (strncmp(chart, OCI, sizeof(OCI) - SKIP_ONE) == 0) {
@@ -300,6 +294,10 @@ static void tf_helm_release(tf_t *t, TSNode block, const char *name) {
if (tf_plain_name(base)) {
snprintf(path, sizeof(path), "%s", base);
external = true;
/* the reference names its repository: all but the chart */
if (!cbm_chart_ref_repo(chart, repo, sizeof(repo))) {
repo[0] = '\0';
}
}
} else if (has_repo) {
if (tf_plain_name(chart)) {
@@ -310,7 +308,7 @@ static void tf_helm_release(tf_t *t, TSNode block, const char *name) {
tgt = tf_local_chart(t, path);
}
if (external) {
tgt = tf_external_chart(t->ctx, path, t->file);
tgt = cbm_pipeline_external_chart(t->ctx, path, repo[0] ? repo : NULL, t->file);
}
if (src <= 0 || tgt <= 0) {
return;
+74 -18
View File
@@ -2539,31 +2539,65 @@ static const char *closure_terraform_decline(char **paths, int n) {
return NULL;
}
/* A role's defaults/ or vars/ main.yml (.yaml): the lists a kube task's loop
* reads (pass_ansible.c, an_role_list) -- a roles/<name> component before. */
static bool closure_ansible_role_vars(const char *rel) {
static const char *const TAILS[] = {"/defaults/main.yml", "/defaults/main.yaml",
"/vars/main.yml", "/vars/main.yaml"};
static const char ROLES[] = "roles/";
size_t n = strlen(rel);
for (size_t k = 0; k < sizeof(TAILS) / sizeof(TAILS[0]); k++) {
size_t tl = strlen(TAILS[k]);
if (n <= tl || strcmp(rel + n - tl, TAILS[k]) != 0) {
continue;
}
const char *end = rel + n - tl; /* the role directory: rel up to end */
for (const char *p = rel; (p = strstr(p, ROLES)) != NULL && p < end; p++) {
if ((p == rel || p[-SKIP_ONE] == '/') && p + sizeof(ROLES) - SKIP_ONE < end) {
return true;
}
}
}
return false;
}
/* Does `rel` hold Ansible tasks (a Function {source: ansible})? -1 on a
* lookup error. */
static int closure_holds_ansible_tasks(cbm_store_t *store, const char *project, const char *rel) {
cbm_node_t *nodes = NULL;
int count = 0;
if (cbm_store_find_nodes_by_file(store, project, rel, &nodes, &count) != CBM_STORE_OK) {
return -SKIP_ONE;
}
int held = 0;
for (int j = 0; j < count && !held; j++) {
held = nodes[j].label && strcmp(nodes[j].label, "Function") == 0 &&
nodes[j].properties_json &&
strstr(nodes[j].properties_json, "\"source\":\"ansible\"") != NULL;
}
cbm_store_free_nodes(nodes, count);
return held;
}
/* Ansible (pass_ansible.c): a task's notify CALLS the handlers of its role
* in another file, and whether a file is Ansible at all can hang on another
* file's include. The repair recomputes the changed files only, so a
* changed or deleted file that held Ansible tasks, or a changed YAML file
* whose top level is a sequence (it may be Ansible now), declines to the
* full rebuild. Returns the reason, or NULL. */
* in another file, whether a file is Ansible at all can hang on another
* file's include, and what a kube task DEPLOYS on the lists of its role's
* defaults / vars. The repair recomputes the changed files only, so a
* changed or deleted file that held Ansible tasks, a changed YAML file whose
* top level is a sequence (it may be Ansible now), or a changed or deleted
* role defaults / vars main file declines to the full rebuild. Returns the
* reason, or NULL. */
static const char *closure_ansible_decline(cbm_store_t *store, const char *project,
const char *repo_path, char **paths, int n_changed,
int n_deleted) {
for (int i = 0; i < n_changed + n_deleted; i++) {
const char *rel = paths[i];
cbm_node_t *nodes = NULL;
int count = 0;
if (cbm_store_find_nodes_by_file(store, project, rel, &nodes, &count) != CBM_STORE_OK) {
return "ansible_lookup_failed";
if (closure_ansible_role_vars(rel)) {
return "ansible_role_vars_changed";
}
bool held = false;
for (int j = 0; j < count && !held; j++) {
held = nodes[j].label && strcmp(nodes[j].label, "Function") == 0 &&
nodes[j].properties_json &&
strstr(nodes[j].properties_json, "\"source\":\"ansible\"");
}
cbm_store_free_nodes(nodes, count);
if (held) {
return "ansible_tasks_changed";
int held = closure_holds_ansible_tasks(store, project, rel);
if (held != 0) {
return held < 0 ? "ansible_lookup_failed" : "ansible_tasks_changed";
}
const char *dot = strrchr(rel, '.');
if (i >= n_changed || !dot || (strcmp(dot, ".yaml") != 0 && strcmp(dot, ".yml") != 0)) {
@@ -2595,6 +2629,24 @@ static const char *closure_ansible_decline(cbm_store_t *store, const char *proje
return NULL;
}
/* A file holding Ansible tasks that the repair would re-walk as a dependent
* (an edge of it reaches a changed or deleted file: a deleted template its
* task DEPLOYS) is walked without the files its edges are drawn from --
* its role's defaults / vars, handlers, included task files -- which are
* outside the closure: it would lose those edges, or keep a dropped one's
* fallback undrawn. Declines to the full rebuild. Returns the reason, or
* NULL. */
static const char *closure_ansible_dependent_decline(cbm_store_t *store, const char *project,
char **dependents, int count) {
for (int i = 0; i < count; i++) {
int held = closure_holds_ansible_tasks(store, project, dependents[i]);
if (held != 0) {
return held < 0 ? "ansible_lookup_failed" : "ansible_dependent";
}
}
return NULL;
}
/* The env join (pass_envjoin.c) reads each image's files from its
* Dockerfile's Module, stored with the ignore patterns of its context: an
* ignore file changing, or a deletion of a file an image copies (which
@@ -2992,6 +3044,10 @@ static int closure_try_plan(cbm_pipeline_t *p, cbm_store_t *store, const char *p
decline = "doc_scope_changed";
goto done;
}
decline = closure_ansible_dependent_decline(store, project, dependents, dependent_count);
if (decline) {
goto done;
}
/* Closure = changed ∪ dependents. Every member must be in the current
* discovery (a dependent outside it cannot be re-resolved). */
+32 -5
View File
@@ -1317,8 +1317,9 @@ char *cbm_k8s_resource_props(const char *qn, const char *effective_ns, const cha
/* Ansible (pass_ansible.c): a playbook or task file IMPORTS the playbooks,
* task files and roles its directives name; its tasks and handlers are
* Functions, a notify CALLS the handlers it names. Run at the end of the
* K8s pass, over the same files. */
* Functions, a notify CALLS the handlers it names, a kube / k8s task
* DEPLOYS the template it applies. Run at the end of the K8s pass, over the
* same files. */
void cbm_pipeline_pass_ansible(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
int file_count);
@@ -1336,6 +1337,30 @@ bool cbm_repo_rel_join(const char *dir, const char *v, char *out, size_t sz);
* digest dropped), made when new; 0 for scratch or no image. */
int64_t cbm_pipeline_image_node(cbm_pipeline_ctx_t *ctx, const char *ref);
/* A chart repository URL as an identity (pass_k8s.c): `url` trimmed, its
* scheme and host lower-cased, trailing `/` and `.git` dropped. False for no
* URL: no scheme or host, a file:// directory, credentials in the authority,
* a query or fragment (`?`, `#`: a token may ride there), or a byte no
* stated URL holds (whitespace, quote, template, `$`). A
* repository URL is never cut: oci://host/x with chart x is oci://host/x/x. */
bool cbm_chart_repo_url(const char *url, char *out, size_t sz);
/* The repository of chart reference `ref` (a Flux OCIRepository url, a
* Terraform `chart = "oci://..."`): all before its last segment, the chart.
* False when none is left. */
bool cbm_chart_ref_repo(const char *ref, char *out, size_t sz);
/* The external Chart node of chart `name` from repository `url` (pass_k8s.c),
* made when new and owned by `file`: `__chart__<url>/<name>` {source,
* external, repository} -- one QN in every project -- when `url` resolves
* (cbm_chart_repo_url), else `<project>.__helm_dep__.<name>`. */
int64_t cbm_pipeline_external_chart(cbm_pipeline_ctx_t *ctx, const char *name, const char *url,
const char *file);
/* Is Chart node `node` an external chart (either identity) rather than one
* a Chart.yaml of this repository declares? */
bool cbm_pipeline_chart_is_external(const cbm_gbuf_node_t *node);
/* Environment variable `key` that `owner` sets (pass_docker.c): a K8s or
* compose Resource for its container `container`, a Dockerfile Module for
* its image (container NULL). Makes the container-scoped EnvVar
@@ -1545,9 +1570,11 @@ int cbm_pipeline_build_fresh_semantic_manifest(cbm_pipeline_t *p, const char *pr
* (pass_terraform.c, extract_unified.c, extract_defs.c); an Ansible
* playbook / task file IMPORTS {via:ansible} the playbooks, task files
* and roles its directives name, its tasks and handlers are Functions
* {source:ansible} and a notify CALLS {via:notify} the handlers it names
* (pass_ansible.c). An index built before holds none of them, so it
* rebuilds once on upgrade. */
* {source:ansible}, a notify CALLS {via:notify} the handlers it names,
* a kube / k8s task DEPLOYS {via:ansible} the template it applies
* (pass_ansible.c). An external Chart is keyed by its publisher URL.
* An index built before holds none of them, so it rebuilds once on
* upgrade. */
enum { CBM_SEMANTIC_INDEX_VERSION = 9 };
typedef struct {
+58 -10
View File
@@ -85,7 +85,8 @@ static void rm_rf(const char *path) {
/* ── Strategy 1: Config Key → Code Symbol ───────────────────────── */
/* Go: TestConfigKeySymbol_ExactMatch
* config.toml has max_connections, main.go has getMaxConnections() */
* config.toml has max_connections, main.go has maxConnections(). (A TOML key
* links by exact name only, see configlink_key_symbol_toml_ini_exact_scoped.) */
TEST(configlink_key_symbol_exact_match) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp/test");
@@ -94,9 +95,9 @@ TEST(configlink_key_symbol_exact_match) {
"test.config.max_connections", "config.toml", 0, 0, NULL);
ASSERT_GT(cfg_id, 0);
/* Code Function node: getMaxConnections from main.go */
int64_t func_id = cbm_gbuf_upsert_node(gb, "Function", "getMaxConnections",
"test.main.getMaxConnections", "main.go", 0, 0, NULL);
/* Code Function node: maxConnections from main.go */
int64_t func_id = cbm_gbuf_upsert_node(gb, "Function", "maxConnections",
"test.main.maxConnections", "main.go", 0, 0, NULL);
ASSERT_GT(func_id, 0);
/* Run configlink */
@@ -113,12 +114,14 @@ TEST(configlink_key_symbol_exact_match) {
}
/* Go: TestConfigKeySymbol_SubstringMatch
* config.toml has request_timeout, main.go has getRequestTimeoutSeconds() */
* app.properties has request_timeout, main.go has getRequestTimeoutSeconds().
* (Formats outside the exact + component rule -- .properties, .env -- keep
* substring matching.) */
TEST(configlink_key_symbol_substring_match) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp/test");
cbm_gbuf_upsert_node(gb, "Variable", "request_timeout", "test.config.request_timeout",
"config.toml", 0, 0, NULL);
cbm_gbuf_upsert_node(gb, "Variable", "request_timeout", "test.app.request_timeout",
"app.properties", 0, 0, NULL);
cbm_gbuf_upsert_node(gb, "Function", "getRequestTimeoutSeconds",
"test.main.getRequestTimeoutSeconds", "main.go", 0, 0, NULL);
@@ -281,6 +284,48 @@ TEST(configlink_key_symbol_terraform_variable) {
PASS();
}
/* TOML and INI keys (.toml, .ini, .cfg, .conf) link like infrastructure keys:
* by exact name and only inside the config file's component. In 17 infra
* repositories all 72 TOML/INI key_symbol edges were substring or
* cross-component coincidences (`.gitleaks.toml useDefault` <-
* TestConfig_UseDefault, `grafana.ini admin_user` <- GenAdminUser). Each
* format's key below reaches only svc-a's exact `requestTimeout`: not svc-a's
* substring `getRequestTimeoutSeconds`, not svc-b's exact `requestTimeout`. */
TEST(configlink_key_symbol_toml_ini_exact_scoped) {
static const char *const files[] = {"svc-a/config.toml", "svc-a/settings.ini",
"svc-a/setup.cfg", "svc-a/app.conf"};
enum { FILES = 4 };
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp/test");
int64_t cfg[FILES];
for (int i = 0; i < FILES; i++) {
char qn[CBM_SZ_128];
snprintf(qn, sizeof(qn), "test.cfg%d.request_timeout", i);
cfg[i] = cbm_gbuf_upsert_node(gb, "Variable", "request_timeout", qn, files[i], 0, 0, NULL);
}
int64_t inside = cbm_gbuf_upsert_node(gb, "Function", "requestTimeout",
"test.svc_a.main.requestTimeout", "svc-a/main.go", 0, 0,
NULL);
cbm_gbuf_upsert_node(gb, "Function", "getRequestTimeoutSeconds",
"test.svc_a.client.getRequestTimeoutSeconds", "svc-a/client.go", 0, 0,
NULL);
cbm_gbuf_upsert_node(gb, "Function", "requestTimeout", "test.svc_b.main.requestTimeout",
"svc-b/main.go", 0, 0, NULL);
run_configlink(gb, "test", NULL);
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_type(gb, "CONFIGURES", &edges, &count);
ASSERT_EQ(count, FILES);
for (int i = 0; i < FILES; i++) {
bool found = false;
for (int e = 0; e < count; e++) {
found = found || (edges[e]->source_id == inside && edges[e]->target_id == cfg[i]);
}
ASSERT_TRUE(found);
}
cbm_gbuf_free(gb);
PASS();
}
/* No count of config keys decides what links: the collector stopped at 4,096. */
TEST(configlink_key_symbol_no_config_cap) {
enum { KEYS = 4200 };
@@ -469,9 +514,11 @@ enum { CL_DET_CANDIDATES = 9000 }; /* > the 8192 code-entry cap */
static cbm_gbuf_t *build_oversized_corpus(bool reverse) {
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp/test");
/* One config key every candidate can match on: "max_connections". */
cbm_gbuf_upsert_node(gb, "Variable", "max_connections", "test.config.max_connections",
"config.toml", 0, 0, NULL);
/* One config key every candidate can match on by substring:
* "max_connections", in a format that keeps substring matching (TOML and
* INI keys link by exact name only). */
cbm_gbuf_upsert_node(gb, "Variable", "max_connections", "test.app.max_connections",
"app.properties", 0, 0, NULL);
for (int k = 0; k < CL_DET_CANDIDATES; k++) {
int i = reverse ? (CL_DET_CANDIDATES - 1 - k) : k;
@@ -529,6 +576,7 @@ SUITE(configlink) {
RUN_TEST(configlink_key_symbol_config_dir_component);
RUN_TEST(configlink_key_symbol_no_config_cap);
RUN_TEST(configlink_key_symbol_terraform_variable);
RUN_TEST(configlink_key_symbol_toml_ini_exact_scoped);
/* Strategy 2: Dependency → Import */
RUN_TEST(configlink_dep_import_package_json);
+236 -1
View File
@@ -1812,7 +1812,7 @@ static char *gpf_deploys(const GpfFile *files, int nfiles) {
"SELECT coalesce(group_concat(x, char(10)), '') FROM (SELECT "
"CASE WHEN s.label IN ('Module','File') THEN '@' || s.file_path ELSE s.name END "
"|| ' -> ' || CASE WHEN t.label='Chart' THEN 'chart:' || t.name || "
"CASE WHEN instr(t.qualified_name,'__helm_dep__') THEN '(ext)' ELSE '' END "
"CASE WHEN json_extract(t.properties,'$.external') THEN '(ext)' ELSE '' END "
"ELSE t.label || ':' || t.file_path END "
"|| ' (' || json_extract(e.properties,'$.via') || ')' AS x "
"FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON t.id=e.target_id "
@@ -3484,6 +3484,117 @@ TEST(probe_ansible_tasks_are_functions) {
PASS();
}
/* Every DEPLOYS an Ansible task draws, as "<task> DEPLOYS <file> [<module>]",
* then "deploys <n>". */
static char *gpf_ansible_deploys(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((SELECT group_concat(x, char(10)) FROM (SELECT s.name || ' DEPLOYS ' "
"|| t.file_path || ' [' || json_extract(e.properties,'$.module') || ']' 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 e.type='DEPLOYS' AND json_extract(e.properties,'$.via')='ansible' "
"ORDER BY x)), '') || char(10) || 'deploys ' || (SELECT COUNT(*) FROM edges WHERE "
"project=?1 AND type='DEPLOYS' AND json_extract(properties,'$.via')='ansible')";
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 kube / k8s task DEPLOYS {via: ansible, module, line} the repository file
* it applies, traced to the template (or copied file) its play rendered it
* from: a renderer whose dest is the very filename; {{ item.item.K }} over
* "{{ R.results }}" -> each item of the renderer registering R (its list
* literal, a set_fact's or the role defaults'); {{ item.K }} -> each of the
* task's own items through the renderer of that dest; a literal filename a
* looped renderer renders to; a k8s template: argument. A template is found
* before a copy to the same dest. A remote copy (even of a name the role's
* files/ holds), a delete by name, apply nothing in the repository. */
TEST(probe_ansible_kube_tasks_deploy_their_templates) {
static const GpfFile files[] = {
{"roles/app/tasks/main.yml",
"- name: Render manifests\n template:\n src: \"{{ item.file }}.j2\"\n"
" dest: \"{{ conf }}/{{ item.file }}\"\n with_items:\n - {name: a, file: a.yml}\n"
" - {name: b, file: b.yml}\n register: app_manifests\n"
"- name: Apply manifests\n kube:\n filename: \"{{ conf }}/{{ item.item.file }}\"\n"
" with_items:\n - \"{{ app_manifests.results }}\"\n"
"- name: Render single\n template:\n src: single.yml.j2\n"
" dest: \"{{ conf }}/single.yml\"\n"
"- name: Apply single\n kube:\n filename: \"{{ conf }}/single.yml\"\n"
"- name: Copy static\n copy:\n src: static.yml\n dest: \"{{ conf }}/static.yml\"\n"
"- name: Apply static\n kube:\n filename: \"{{ conf }}/static.yml\"\n"
"- name: Render from defaults\n template:\n src: \"{{ item.file }}.j2\"\n"
" dest: \"{{ conf }}/x/{{ item.file }}\"\n loop: \"{{ app_extra }}\"\n"
" register: extra_out\n"
"- name: Apply from defaults\n kube:\n filename: \"{{ conf }}/x/{{ item.item.file }}\"\n"
" loop: \"{{ extra_out.results }}\"\n"
"- name: Facts\n set_fact:\n sf_list:\n - {file: e.yml}\n"
"- name: Render from facts\n template:\n src: \"{{ item.file }}.j2\"\n"
" dest: \"{{ conf }}/f/{{ item.file }}\"\n with_items: \"{{ sf_list }}\"\n"
" register: sf_out\n"
"- name: Apply set_fact\n kube:\n filename: \"{{ conf }}/f/{{ item.item.file }}\"\n"
" with_items: \"{{ sf_out.results }}\"\n"
"- name: Pairs\n template:\n src: \"{{ item.name }}/{{ item.file }}.j2\"\n"
" dest: \"{{ conf }}/{{ item.name }}-{{ item.file }}\"\n with_items:\n"
" - {name: c, file: c.yml}\n"
"- name: Apply pairs\n kube:\n filename: \"{{ conf }}/{{ item.name }}-{{ item.file }}\"\n"
" loop:\n - {name: c, file: c.yml}\n"
"- name: Rendered literal\n template:\n src: \"{{ item.file }}.j2\"\n"
" dest: \"{{ conf }}/r/{{ item.file }}\"\n with_items:\n - {file: ipam.yml}\n"
"- name: Apply literal\n kube:\n filename: \"{{ conf }}/r/ipam.yml\"\n"
"- name: Copy first\n copy:\n src: first.yml\n dest: \"{{ conf }}/dup.yml\"\n"
"- name: Render dup\n template:\n src: dup.yml.j2\n dest: \"{{ conf }}/dup.yml\"\n"
"- name: Apply dup\n kube:\n filename: \"{{ conf }}/dup.yml\"\n"
"- name: Remote copy\n copy:\n src: static.yml\n dest: \"{{ conf }}/remote.yml\"\n"
" remote_src: true\n"
"- name: Apply remote\n kube:\n filename: \"{{ conf }}/remote.yml\"\n"
"- name: Delete by name\n kube:\n name: old\n resource: deploy\n state: absent\n"
"- name: k8s template\n kubernetes.core.k8s:\n template: tpl.yml.j2\n"},
{"roles/app/defaults/main.yml", "app_extra:\n - file: d.yml\nother: 1\n"},
{"roles/app/templates/a.yml.j2", "kind: ConfigMap\n"},
{"roles/app/templates/b.yml.j2", "kind: ConfigMap\n"},
{"roles/app/templates/c/c.yml.j2", "kind: ConfigMap\n"},
{"roles/app/templates/d.yml.j2", "kind: ConfigMap\n"},
{"roles/app/templates/e.yml.j2", "kind: ConfigMap\n"},
{"roles/app/templates/ipam.yml.j2", "kind: ConfigMap\n"},
{"roles/app/templates/single.yml.j2", "kind: ConfigMap\n"},
{"roles/app/templates/tpl.yml.j2", "kind: ConfigMap\n"},
{"roles/app/templates/dup.yml.j2", "kind: ConfigMap\n"},
{"roles/app/files/first.yml", "kind: ConfigMap\n"},
{"roles/app/files/static.yml", "kind: ConfigMap\n"}};
int n = (int)(sizeof(files) / sizeof(files[0]));
char *got = gpf_ansible_deploys(files, n);
static const char want[] =
"Apply dup DEPLOYS roles/app/templates/dup.yml.j2 [kube]\n"
"Apply from defaults DEPLOYS roles/app/templates/d.yml.j2 [kube]\n"
"Apply literal DEPLOYS roles/app/templates/ipam.yml.j2 [kube]\n"
"Apply manifests DEPLOYS roles/app/templates/a.yml.j2 [kube]\n"
"Apply manifests DEPLOYS roles/app/templates/b.yml.j2 [kube]\n"
"Apply pairs DEPLOYS roles/app/templates/c/c.yml.j2 [kube]\n"
"Apply set_fact DEPLOYS roles/app/templates/e.yml.j2 [kube]\n"
"Apply single DEPLOYS roles/app/templates/single.yml.j2 [kube]\n"
"Apply static DEPLOYS roles/app/files/static.yml [kube]\n"
"k8s template DEPLOYS roles/app/templates/tpl.yml.j2 [kubernetes.core.k8s]\n"
"deploys 10";
if (!got || strcmp(got, want) != 0) {
fprintf(stderr, "ansible deploys got:\n%s\nwant:\n%s\n", got ? got : "(null)", want);
free(got);
FAIL("ansible deploys");
}
free(got);
PASS();
}
/* Every script HTTP_CALLS edge as "<source file> <route QN>", then a line
* "edgeless routes <n>": the Route nodes no edge touches. */
static char *gpf_script_http(const GpfFile *files, int nfiles) {
@@ -4652,6 +4763,128 @@ TEST(probe_helm_unresolved_file_dependency_stays_external) {
PASS();
}
/* Every external Chart as "<name> <QN, the project as <p>> <repository or ->
* in=<edges into it>", sorted, one per line. NULL on failure; the caller
* frees. */
static char *gpf_external_charts(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 n.name || ' ' || "
"replace(n.qualified_name, ?1, '<p>') || ' ' || "
"coalesce(json_extract(n.properties,'$.repository'), '-') || ' in=' || "
"(SELECT count(*) FROM edges e WHERE e.target_id=n.id) AS x FROM nodes n "
"WHERE n.project=?1 AND n.label='Chart' AND json_extract(n.properties,'$.external') "
"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;
}
/* An external Chart is keyed by its publisher, __chart__<repository>/<name>,
* the repository's scheme and host lower-cased, a trailing slash and `.git`
* dropped: a Chart.yaml dependency, a kustomization, a Flux chartRef and a
* Terraform oci:// reference naming one published chart meet in one node,
* the same QN in every project. An oci:// repository is never cut, even when
* it ends in the chart's name (karpenter); a chart reference is cut to its
* repository. A repository that is no URL (an @alias, no scheme, file://,
* credentials in the authority, a query or fragment a token may ride in, an
* unexpanded ${...}) or that the pass does not read (a
* Terraform `repository =`) keeps the per-project
* <project>.__helm_dep__.<name>. The external charts a Chart.yaml depends on
* carry its path, yet a helm_release, a helm command and a Go loader naming
* that chart directory deploy the chart the directory declares. */
TEST(probe_external_chart_identity_is_its_publisher) {
static const GpfFile files[] = {
{"charts/app/Chart.yaml",
"apiVersion: v2\nname: app\nversion: 1.0.0\ndependencies:\n"
" - name: redis\n repository: https://Charts.Example.com/stable/\n"
" version: 1.x\n"
" - name: common\n repository: \"@stable\"\n version: 1.x\n"
" - name: nats\n repository: oci://registry.example.com/charts\n"
" version: 1.x\n"
" - name: vault\n repository: https://user@charts.example.com/private\n"
" version: 1.x\n"},
{"main.tf", "resource \"helm_release\" \"app\" {\n"
" name = \"app\"\n chart = \"./charts/app\"\n}\n\n"
"resource \"helm_release\" \"queue\" {\n"
" name = \"queue\"\n"
" chart = \"oci://registry.example.com/charts/nats\"\n}\n\n"
"resource \"helm_release\" \"cache\" {\n"
" name = \"cache\"\n"
" repository = \"https://charts.example.com/stable\"\n"
" chart = \"redis\"\n}\n"},
{"deploy.sh", "#!/bin/sh\ndeploy_app() {\n helm upgrade --install app ./charts/app\n}\n"},
{"go.mod", "module example.com/app\n\ngo 1.22\n"},
{"install.go", "package main\n\n"
"import \"helm.sh/helm/v3/pkg/chart/loader\"\n\n"
"func installApp() {\n\t_, _ = loader.Load(\"charts/app\")\n}\n"},
{"obs/kustomization.yaml",
GPF_KUST "helmCharts:\n- name: redis\n repo: https://charts.example.com/stable.git\n"
"- name: karpenter\n repo: oci://registry.example.com/karpenter\n"
"- name: templated\n repo: https://charts.example.com/${ENV}\n"
"- name: local\n repo: file://../charts\n"
"- name: bare\n repo: charts.example.com/stable\n"
"- name: tokened\n repo: https://charts.example.com/stable?token=x\n"
"- name: fragment\n repo: https://charts.example.com/stable#x\n"},
{"apps/web/kustomization.yaml",
GPF_KUST "namespace: web\nresources:\n - ocirepository.yaml\n - helmrelease.yaml\n"},
{"apps/web/ocirepository.yaml",
"apiVersion: source.toolkit.fluxcd.io/v1\nkind: OCIRepository\nmetadata:\n"
" name: web-chart\nspec:\n url: oci://registry.example.com/charts/nats\n"},
{"apps/web/helmrelease.yaml",
"apiVersion: helm.toolkit.fluxcd.io/v2\nkind: HelmRelease\nmetadata:\n name: web\n"
"spec:\n chartRef:\n kind: OCIRepository\n name: web-chart\n"}};
int n = (int)(sizeof(files) / sizeof(files[0]));
char *got = gpf_external_charts(files, n);
static const char want[] =
"bare <p>.__helm_dep__.bare - in=1\n"
"common <p>.__helm_dep__.common - in=1\n"
"fragment <p>.__helm_dep__.fragment - in=1\n"
"karpenter __chart__oci://registry.example.com/karpenter/karpenter "
"oci://registry.example.com/karpenter in=1\n"
"local <p>.__helm_dep__.local - in=1\n"
"nats __chart__oci://registry.example.com/charts/nats oci://registry.example.com/charts "
"in=3\n"
"redis <p>.__helm_dep__.redis - in=1\n"
"redis __chart__https://charts.example.com/stable/redis https://charts.example.com/stable "
"in=2\n"
"templated <p>.__helm_dep__.templated - in=1\n"
"tokened <p>.__helm_dep__.tokened - in=1\n"
"vault <p>.__helm_dep__.vault - in=1";
if (!got || strcmp(got, want) != 0) {
fprintf(stderr, "external charts got:\n%s\nwant:\n%s\n", got ? got : "(null)", want);
free(got);
FAIL("external chart identity");
}
free(got);
GPF_ASSERT_DEPLOYS(files, n,
"HelmRelease/web -> chart:nats(ext) (helmrelease)\n"
"Kustomization -> chart:bare(ext) (helmcharts)\n"
"Kustomization -> chart:fragment(ext) (helmcharts)\n"
"Kustomization -> chart:karpenter(ext) (helmcharts)\n"
"Kustomization -> chart:local(ext) (helmcharts)\n"
"Kustomization -> chart:redis(ext) (helmcharts)\n"
"Kustomization -> chart:templated(ext) (helmcharts)\n"
"Kustomization -> chart:tokened(ext) (helmcharts)\n"
"deploy_app -> chart:app (helm)\n"
"installApp -> chart:app (helm-sdk)\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();
}
/* A remote base is outside the repository. The import resolver's symbol-name
* guess walked its URL segments and linked kubermatic's gcp addon to the
* `deploy` function of hack/ci/deploy.sh. A kustomize entry is a path,
@@ -5869,6 +6102,7 @@ SUITE(grammar_probe_f) {
RUN_TEST(probe_k8s_manifest_mints_no_envelope_variables);
RUN_TEST(probe_yaml_config_keeps_variables);
RUN_TEST(probe_helm_unresolved_file_dependency_stays_external);
RUN_TEST(probe_external_chart_identity_is_its_publisher);
RUN_TEST(probe_kustomize_nested_sibling_entries);
RUN_TEST(probe_kustomize_single_module);
RUN_TEST(probe_kustomize_entries_after_commented_out_entry);
@@ -5930,6 +6164,7 @@ SUITE(grammar_probe_f) {
RUN_TEST(probe_terraform_kubernetes_objects_are_resources);
RUN_TEST(probe_kustomize_name_prefix_makes_variants);
RUN_TEST(probe_ansible_tasks_are_functions);
RUN_TEST(probe_ansible_kube_tasks_deploy_their_templates);
RUN_TEST(probe_image_hub_splits_by_component);
RUN_TEST(probe_gitops_objects_deploy_their_units);
RUN_TEST(probe_shell_kubectl_deploys_what_it_applies);
+90
View File
@@ -1592,6 +1592,95 @@ TEST(pipeline_incremental_keeps_ansible_notifies_across_files) {
PASS();
}
/* The files Ansible tasks DEPLOY (via ansible), ordered, one per line, into
* `out`; false on a query error. */
static bool ansible_deploy_targets(cbm_store_t *s, const char *project, char *out, size_t sz) {
sqlite3_stmt *st = NULL;
bool ok = false;
if (sqlite3_prepare_v2(cbm_store_get_db(s),
"SELECT coalesce(group_concat(p, char(10)), '') FROM (SELECT "
"t.file_path AS p FROM edges e JOIN nodes t ON t.id=e.target_id WHERE "
"e.project=?1 AND e.type='DEPLOYS' AND "
"json_extract(e.properties,'$.via')='ansible' ORDER BY p)",
-1, &st, NULL) == SQLITE_OK &&
sqlite3_bind_text(st, 1, project, -1, SQLITE_TRANSIENT) == SQLITE_OK &&
sqlite3_step(st) == SQLITE_ROW) {
snprintf(out, sz, "%s", (const char *)sqlite3_column_text(st, 0));
ok = true;
}
sqlite3_finalize(st);
return ok;
}
/* A kube task DEPLOYS the template its play rendered, read through a list of
* the role's defaults/main.yml -- a file holding no task. Editing it (the
* list names another template) or deleting it must land on the full graph,
* and so must deleting the deployed template while a file of that name sits
* in the task file's directory (the render falls back to it). The repair
* did not re-walk the task file for the list edit -- it kept the edge to the
* template the list no longer names -- and re-walked it for the deletion as
* a dependent of the template, without the role's defaults: no edge at all.
* Both decline to the full rebuild (closure_ansible_decline: a role's
* defaults / vars; closure_ansible_dependent_decline: a task file joining
* the repair as a dependent). A template body edit keeps the repair and the
* edge. */
TEST(pipeline_incremental_keeps_ansible_deploys_across_files) {
if (setup_test_repo() != 0) {
FAIL("failed to create temp dir");
}
write_temp_file(g_tmpdir, "roles/app/tasks/main.yml",
"- name: Render\n template:\n src: \"{{ item.file }}.j2\"\n"
" dest: \"/etc/k/{{ item.file }}\"\n loop: \"{{ app_list }}\"\n"
" register: out\n"
"- name: Apply\n kube:\n filename: \"/etc/k/{{ item.item.file }}\"\n"
" loop: \"{{ out.results }}\"\n");
write_temp_file(g_tmpdir, "roles/app/defaults/main.yml", "app_list:\n - {file: a.yml}\n");
write_temp_file(g_tmpdir, "roles/app/templates/a.yml.j2", "a: 1\n");
write_temp_file(g_tmpdir, "roles/app/templates/b.yml.j2", "b: 1\n");
write_temp_file(g_tmpdir, "roles/app/tasks/b.yml.j2", "b: 2\n");
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/test_incr_deploys.db", g_tmpdir);
static const char *const want[] = {
"roles/app/templates/a.yml.j2", /* the full index */
"roles/app/templates/a.yml.j2", /* a.yml.j2's body edited */
"roles/app/templates/b.yml.j2", /* the defaults list names b.yml */
"roles/app/tasks/b.yml.j2", /* templates/b.yml.j2 deleted */
"", /* the defaults deleted */
};
char path[1024];
for (int step = 0; step < (int)(sizeof(want) / sizeof(want[0])); step++) {
if (step == 1) {
snprintf(path, sizeof(path), "%s/roles/app/templates/a.yml.j2", g_tmpdir);
ASSERT_EQ(th_append_file(path, "a2: 2\n"), 0);
} else if (step == 2) {
write_temp_file(g_tmpdir, "roles/app/defaults/main.yml",
"app_list:\n - {file: b.yml}\n");
} else if (step == 3) {
snprintf(path, sizeof(path), "%s/roles/app/templates/b.yml.j2", g_tmpdir);
ASSERT_EQ(th_unlink_force(path), 0);
} else if (step == 4) {
snprintf(path, sizeof(path), "%s/roles/app/defaults/main.yml", g_tmpdir);
ASSERT_EQ(th_unlink_force(path), 0);
}
cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
char got[2048];
bool ok = ansible_deploy_targets(s, cbm_pipeline_project_name(p), got, sizeof(got));
cbm_store_close(s);
cbm_pipeline_free(p);
ASSERT_TRUE(ok);
if (strcmp(got, want[step]) != 0) {
fprintf(stderr, "step %d deploys got [%s] want [%s]\n", step, got, want[step]);
}
ASSERT_STR_EQ(got, want[step]);
}
teardown_test_repo();
PASS();
}
/* Regression: a Terraform module's output read lives in one file
* (infra/outputs.tf: module.net.vpc_id) and the module's source in another
* (infra/main.tf). Pointing the source elsewhere edits main.tf alone; the
@@ -23295,6 +23384,7 @@ SUITE(pipeline) {
RUN_TEST(pipeline_incremental_drops_terraform_reads_of_a_moved_module);
RUN_TEST(pipeline_incremental_keeps_ansible_includes_of_an_edited_file);
RUN_TEST(pipeline_incremental_keeps_ansible_notifies_across_files);
RUN_TEST(pipeline_incremental_keeps_ansible_deploys_across_files);
RUN_TEST(pipeline_field_named_like_method_keeps_its_reads_and_writes);
RUN_TEST(pipeline_objectscript_export_preserves_calls_sequential_parallel);
RUN_TEST(pipeline_objectscript_export_incremental_matches_full_relationships);