feat(infra): a Resource keeps its labels; a patch target selects by them

A Kubernetes object's metadata.labels were not in the graph: neither a
search for an object by label nor a kustomize patch whose target selects
by label could find anything.

A Resource now keeps its labels as `labels: "k=v,k2=v2"` (sorted; a
templated label, or one a selector cannot name, is left out), and a
patch target's labelSelector meets the objects of its build carrying its
terms -- `k=v` / `k==v`, `k!=v`, `k`, `!k`; a set-based term names no key
and meets nothing. A target with only a selector selects among every
kind; an annotationSelector is not evaluated and names nothing.

Measured on 17 infrastructure repositories: 1,956 Resources gain labels
and nothing else; an independent reading of metadata.labels agrees on
every one of the 1,804 objects both read (152 more are Go-templated,
which only the graph reads, through its skeleton). The 4 label-selector
patch pairs now resolve: kustomize patches 130 of 130.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-14 02:27:54 +02:00
1 parent 35833e1f91
commit e58c5ba730
6 files changed
+232 -45

No files matched your search

+2 -2
View File
@@ -255,7 +255,7 @@ The install script placed beside the binary is **reported, not deleted** — uni
- `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 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`, ...)
- `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`
- `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); 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: 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
- `EMITS`, `LISTENS_ON` (channels)
- `DATA_FLOWS` with arg-to-param mapping + field access chains
- `SIMILAR_TO` (MinHash + LSH near-clone detection, Jaccard scored)
@@ -264,7 +264,7 @@ The install script placed beside the binary is **reported, not deleted** — uni
### Indexing pipeline
- **158 vendored tree-sitter grammars** compiled into the binary
- **Generic package / module resolution** — bare specifiers like `@myorg/pkg`, `github.com/foo/bar`, `use my_crate::foo` resolved via manifest scanning (`package.json`, `go.mod`, `Cargo.toml`, `pyproject.toml`, `composer.json`, `pubspec.yaml`, `pom.xml`, `build.gradle`, `mix.exs`, `*.gemspec`)
- **Infrastructure-as-code indexing** — Dockerfiles (`Dockerfile.*`, `*.Dockerfile`, `Containerfile`: bases, what the container runs, `ENV`), container images (`Image`: the normalised repository, tag and digest on the edges; a repository built from several Dockerfiles, one per component tag, gets an `Image` per component), docker compose services (`Resource`), Kubernetes manifests (`Resource`, with `group`, `version`, `kind`, `namespace`), Kustomize overlays (`IMPORTS` to their entries and patch files, `OVERRIDE` from their patches), Helm charts (`Chart`, `DEPENDS_ON`), Terraform / OpenTofu (`.tf`, `.tofu`, `.tfvars`) as graph nodes. A Service walks to the code it runs: Service → workload → `Image` → Dockerfile → `main`
- **Infrastructure-as-code indexing** — Dockerfiles (`Dockerfile.*`, `*.Dockerfile`, `Containerfile`: bases, what the container runs, `ENV`), container images (`Image`: the normalised repository, tag and digest on the edges; a repository built from several Dockerfiles, one per component tag, gets an `Image` per component), docker compose services (`Resource`), Kubernetes manifests (`Resource`, with `group`, `version`, `kind`, `namespace`, `labels` as `k=v,k2=v2`), Kustomize overlays (`IMPORTS` to their entries and patch files, `OVERRIDE` from their patches), Helm charts (`Chart`, `DEPENDS_ON`), Terraform / OpenTofu (`.tf`, `.tofu`, `.tfvars`) as graph nodes. A Service walks to the code it runs: Service → workload → `Image` → Dockerfile → `main`
- **[Hybrid LSP semantic type resolution](#hybrid-lsp)** for Python, TypeScript / JavaScript / JSX / TSX, PHP, C#, Go, C, C++, Java, Kotlin, Rust, and Perl — a lightweight C implementation of language type-resolution algorithms, structurally inspired by and compatible with major language servers including tsserver / typescript-go, pyright, gopls, Roslyn, Eclipse JDT, and rust-analyzer (parameter binding, return-type inference, generic substitution, JSX component dispatch, JSDoc inference for plain JS files, namespace + trait + late-static-binding resolution for PHP, file-scoped namespaces + records + LINQ method syntax for C#, class-hierarchy + overload + lambda resolution for Java, extension-function + scope-function resolution for Kotlin, trait-method + UFCS resolution for Rust)
- **RAM-first pipeline**: LZ4 compression, in-memory SQLite, single dump at end. Memory released after.
+7 -6
View File
@@ -750,12 +750,13 @@ typedef struct {
const char *edge;
const char *route;
const char *container;
const char *key; /* CONNECTS_TO: the environment variable addressing it */
const char *group; /* PATCH: the API group named ("core"), NULL: any */
const char *version; /* PATCH: the API version named, NULL: any */
const char *path; /* PATCH: the patch file, NULL: inline */
const char *url; /* DEPLOY (kustomize): the chart repository */
const char *release; /* DEPLOY (kustomize): the release name */
const char *key; /* CONNECTS_TO: the environment variable addressing it */
const char *group; /* PATCH: the API group named ("core"), NULL: any */
const char *version; /* PATCH: the API version named, NULL: any */
const char *path; /* PATCH: the patch file, NULL: inline */
const char *selector; /* PATCH: the target's labelSelector, NULL: none */
const char *url; /* DEPLOY (kustomize): the chart repository */
const char *release; /* DEPLOY (kustomize): the release name */
int line;
} CBMK8sFact;
+91 -14
View File
@@ -23,7 +23,7 @@
/* Local constants. */
enum {
K8S_BUF_SIZE = 256,
K8S_PROPS_KV = 3, /* extra_props: one key, its value, the NULL end */
K8S_PROPS_KV = 5, /* extra_props: version, labels (key + value each), NULL */
};
// ---------------------------------------------------------------------------
@@ -237,7 +237,8 @@ typedef struct {
const char *version;
const char *name;
const char *ns;
bool selector; /* labelSelector / annotationSelector */
const char *selector; /* labelSelector */
bool unsupported; /* annotationSelector: not evaluated */
} kust_target_t;
static void kust_target(CBMExtractCtx *ctx, TSNode map, kust_target_t *t) {
@@ -264,8 +265,10 @@ static void kust_target(CBMExtractCtx *ctx, TSNode map, kust_target_t *t) {
t->name = val;
} else if (strcmp(key, "namespace") == 0) {
t->ns = val;
} else if (strcmp(key, "labelSelector") == 0 || strcmp(key, "annotationSelector") == 0) {
t->selector = true;
} else if (strcmp(key, "labelSelector") == 0) {
t->selector = val;
} else if (strcmp(key, "annotationSelector") == 0) {
t->unsupported = true;
}
}
}
@@ -436,13 +439,14 @@ static void emit_kustomize_patches(CBMExtractCtx *ctx, TSNode seq) {
.path = path && path[0] ? path : NULL,
.line = (int)ts_node_start_point(item).row + TS_LINE_OFFSET};
if (has_target) {
if (!t.selector && (t.kind || t.name)) {
if (!t.unsupported && (t.kind || t.name || t.selector)) {
CBMK8sFact f = base;
f.kind = t.kind;
f.group = t.group;
f.version = t.version;
f.name = t.name;
f.ns = t.ns;
f.selector = t.selector;
cbm_k8sfact_push(&ctx->result->k8s_facts, a, f);
}
} else if (inline_text) {
@@ -613,8 +617,72 @@ typedef struct {
char kind[K8S_BUF_SIZE];
char name[K8S_BUF_SIZE];
char ns[K8S_BUF_SIZE];
const char *labels; // metadata.labels as "k=v,k2=v2" (text order), NULL: none
} k8s_doc_id_t;
// A label a selector term can name: no templated part (a skeleton's `<...>`,
// `{{`), no `,` or `=` (the separators of the list), nothing JSON escapes
// (`"`, `\`, control characters -- none in a valid label).
static bool k8s_label_plain(const char *s) {
if (s[0] == '<' || strstr(s, "{{") || strpbrk(s, ",=\"\\")) {
return false;
}
for (const char *p = s; *p; p++) {
if ((unsigned char)*p < ' ') {
return false;
}
}
return true;
}
static int k8s_cmp_label(const void *x, const void *y) {
return strcmp(*(const char *const *)x, *(const char *const *)y);
}
// metadata.labels as "k=v,k2=v2" in text order (the arena's), NULL when it
// has none a selector can name.
static const char *k8s_labels(CBMArena *a, TSNode map, const char *source) {
if (strcmp(ts_node_type(map), "block_mapping") != 0) {
return NULL;
}
int cap = (int)ts_node_named_child_count(map);
const char **items = cap > 0 ? cbm_arena_alloc(a, (size_t)cap * sizeof(*items)) : NULL;
int n = 0;
size_t len = 0;
k8s_kids_t pairs = k8s_kids(map);
TSNode p;
while (items && n < cap && k8s_kid(&pairs, false, &p)) {
if (strcmp(ts_node_type(p), "block_mapping_pair") != 0) {
continue;
}
const char *k = get_scalar_text(a, ts_node_named_child(p, 0), source);
TSNode vn = unwrap_pair_value(p);
const char *v = ts_node_is_null(vn) ? NULL : get_scalar_text(a, vn, source);
if (k && k[0] && v && k8s_label_plain(k) && k8s_label_plain(v)) {
items[n] = cbm_arena_sprintf(a, "%s=%s", k, v);
len += items[n] ? strlen(items[n]) + SKIP_ONE : 0;
n += items[n] ? SKIP_ONE : 0;
}
}
k8s_kids_done(&pairs);
if (n == 0) {
return NULL;
}
qsort((void *)items, (size_t)n, sizeof(*items), k8s_cmp_label);
char *out = cbm_arena_alloc(a, len);
if (!out) {
return NULL;
}
size_t w = 0;
for (int i = 0; i < n; i++) {
size_t l = strlen(items[i]);
memcpy(out + w, items[i], l);
w += l;
out[w++] = i + SKIP_ONE < n ? ',' : '\0';
}
return out;
}
// Read metadata.name and metadata.namespace from a metadata block_mapping.
static void extract_metadata(CBMArena *a, TSNode meta_mapping, const char *source,
k8s_doc_id_t *id) {
@@ -634,10 +702,14 @@ static void extract_metadata(CBMArena *a, TSNode meta_mapping, const char *sourc
const char *mkey_text = get_scalar_text(a, mkey, source);
bool is_name = mkey_text && strcmp(mkey_text, "name") == 0;
bool is_ns = mkey_text && strcmp(mkey_text, "namespace") == 0;
TSNode mval = unwrap_pair_value(mpair);
if (mkey_text && strcmp(mkey_text, "labels") == 0 && !ts_node_is_null(mval)) {
id->labels = k8s_labels(a, mval, source);
continue;
}
if (!is_name && !is_ns) {
continue;
}
TSNode mval = unwrap_pair_value(mpair);
const char *text = ts_node_is_null(mval) ? NULL : get_scalar_text(a, mval, source);
if (text) {
snprintf(is_name ? id->name : id->ns, K8S_BUF_SIZE, "%s", text);
@@ -1658,14 +1730,19 @@ static void extract_k8s_manifest(CBMExtractCtx *ctx) {
const char *slash = strchr(id.api_version, '/');
const char *version = slash ? slash + SKIP_ONE : id.api_version;
bool versioned = slash || (version[0] == 'v' && isdigit((unsigned char)version[1]));
if (version[0] && versioned && id.api_version[0] != '<') {
const char **kv = (const char **)cbm_arena_alloc(a, K8S_PROPS_KV * sizeof(char *));
if (kv) {
kv[0] = "version";
kv[SKIP_ONE] = cbm_arena_strdup(a, version);
kv[PAIR_LEN] = NULL;
def.extra_props = kv;
}
const char **kv = (const char **)cbm_arena_alloc(a, K8S_PROPS_KV * sizeof(char *));
int kn = 0;
if (kv && version[0] && versioned && id.api_version[0] != '<') {
kv[kn++] = "version";
kv[kn++] = cbm_arena_strdup(a, version);
}
if (kv && id.labels) {
kv[kn++] = "labels";
kv[kn++] = id.labels;
}
if (kv && kn > 0) {
kv[kn] = NULL;
def.extra_props = kv;
}
cbm_defs_push(&ctx->result->defs, a, def);
k8s_emit_facts(ctx, mapping, group, id.kind, ctx->result->defs.count - SKIP_ONE);
+108 -16
View File
@@ -552,10 +552,12 @@ static bool k8s_cluster_scoped(const char *kind) {
* `obj_key` is the logical identity: the namespace it lives in, `_` for a
* cluster-scoped kind, `?` when the deploy decides; `effective_ns` (when
* not NULL) is the namespace a kustomize build gives it. `version` (NULL or
* "": none) is the version of its apiVersion, which the key leaves out. NULL
* for a QN without a well-formed key (the caller keeps the plain source
* tag). */
static char *k8s_resource_props(const char *qn, const char *effective_ns, const char *version) {
* "": none) is the version of its apiVersion, which the key leaves out;
* `labels` (NULL: none) its metadata.labels as "k=v,k2=v2" (extract_k8s.c:
* nothing JSON escapes in it). NULL for a QN without a well-formed key (the
* caller keeps the plain source tag). */
static char *k8s_resource_props(const char *qn, const char *effective_ns, const char *version,
const char *labels) {
enum { K8S_KEY_PARTS = 4, K8S_ESC = 6 * K8S_LABEL_LEN };
const char *key = qn ? strstr(qn, "::") : NULL;
if (!key) {
@@ -589,7 +591,8 @@ static char *k8s_resource_props(const char *qn, const char *effective_ns, const
cbm_json_escape(esc[3], K8S_ESC, name);
cbm_json_escape(esc[4], K8S_ESC, effective);
cbm_json_escape(esc[5], K8S_ESC, version ? version : "");
size_t n = K8S_KEY_PARTS * K8S_ESC + PAIR_LEN * K8S_ESC + CBM_SZ_256;
size_t n =
K8S_KEY_PARTS * K8S_ESC + PAIR_LEN * K8S_ESC + CBM_SZ_256 + (labels ? strlen(labels) : 0);
char *props = cbm_alloc(CBM_MEM_CLASS_OTHER, n);
if (!props) {
return NULL;
@@ -599,6 +602,9 @@ static char *k8s_resource_props(const char *qn, const char *effective_ns, const
if (esc[5][0]) {
w += snprintf(props + w, n - (size_t)w, ",\"version\":\"%s\"", esc[5]);
}
if (labels && labels[0]) {
w += snprintf(props + w, n - (size_t)w, ",\"labels\":\"%s\"", labels);
}
if (ns[0]) {
w += snprintf(props + w, n - (size_t)w, ",\"namespace\":\"%s\"", esc[2]);
}
@@ -621,6 +627,25 @@ static void k8s_node_prop(const cbm_gbuf_node_t *n, const char *key, char *out,
}
}
/* The string property `key` of node `n` as k8s_resource_props writes it,
* any length (caller frees), NULL when it has none. */
static char *k8s_node_prop_dup(const cbm_gbuf_node_t *n, const char *key) {
char head[CBM_SZ_64];
snprintf(head, sizeof(head), "\"%s\":\"", key);
const char *v = n->properties_json ? strstr(n->properties_json, head) : NULL;
const char *e = v ? strchr(v + strlen(head), '"') : NULL;
if (!e) {
return NULL;
}
v += strlen(head);
char *out = cbm_alloc(CBM_MEM_CLASS_OTHER, (size_t)(e - v) + SKIP_ONE);
if (out) {
memcpy(out, v, (size_t)(e - v));
out[e - v] = '\0';
}
return out;
}
static void k8s_records_free(k8s_record_array_t *recs) {
for (int i = 0; i < recs->pair_count; i++) {
cbm_free(CBM_MEM_CLASS_OTHER, recs->pairs[i]);
@@ -1147,7 +1172,9 @@ static void k8s_apply_effective_namespaces(cbm_pipeline_ctx_t *ctx, k8s_bundles_
}
char version[K8S_LABEL_LEN];
k8s_node_prop(n, "version", version, sizeof(version));
char *props = k8s_resource_props(n->qualified_name, agree ? eff : "?", version);
char *labels = k8s_node_prop_dup(n, "labels");
char *props = k8s_resource_props(n->qualified_name, agree ? eff : "?", version, labels);
cbm_free(CBM_MEM_CLASS_OTHER, labels);
if (props) {
(void)cbm_gbuf_node_set_properties_json((cbm_gbuf_node_t *)n, props);
cbm_free(CBM_MEM_CLASS_OTHER, props);
@@ -1223,6 +1250,7 @@ typedef struct {
char *version; /* patch file name them */
char *name;
char *ns;
char *selector; /* labelSelector, NULL: none */
int line;
} k8s_patch_t;
@@ -1309,6 +1337,7 @@ static void k8s_facts_free(k8s_facts_t *f) {
cbm_free(CBM_MEM_CLASS_OTHER, p->version);
cbm_free(CBM_MEM_CLASS_OTHER, p->name);
cbm_free(CBM_MEM_CLASS_OTHER, p->ns);
cbm_free(CBM_MEM_CLASS_OTHER, p->selector);
}
cbm_free(CBM_MEM_CLASS_OTHER, f->patches);
for (int i = 0; i < f->kchart_n; i++) {
@@ -2389,12 +2418,16 @@ static void handle_k8s_manifest(cbm_pipeline_ctx_t *ctx, const char *rel_path, c
continue;
}
const char *version = NULL; /* extract_k8s.c: the one extra property */
if (def->extra_props && def->extra_props[0] && def->extra_props[SKIP_ONE] &&
strcmp(def->extra_props[0], "version") == 0) {
version = def->extra_props[SKIP_ONE];
const char *version = NULL; /* extract_k8s.c's extra properties */
const char *labels = NULL;
for (const char **kv = def->extra_props; kv && kv[0] && kv[SKIP_ONE]; kv += PAIR_LEN) {
if (strcmp(kv[0], "version") == 0) {
version = kv[SKIP_ONE];
} else if (strcmp(kv[0], "labels") == 0) {
labels = kv[SKIP_ONE];
}
}
char *props = k8s_resource_props(def->qualified_name, NULL, version);
char *props = k8s_resource_props(def->qualified_name, NULL, version, labels);
int64_t node_id = cbm_gbuf_upsert_node(
ctx->gbuf, "Resource", def->name, def->qualified_name, rel_path, (int)def->start_line,
(int)def->end_line, props ? props : "{\"source\":\"k8s\"}");
@@ -3582,6 +3615,7 @@ static void k8s_kustomize_keep(k8s_facts_t *f, const char *kust, const CBMFileRe
.version = k8s_dup_opt(x->version),
.name = k8s_dup_opt(x->name),
.ns = k8s_dup_opt(x->ns),
.selector = k8s_dup_opt(x->selector),
.line = x->line};
}
}
@@ -3614,8 +3648,64 @@ typedef struct {
const char *version;
const char *name;
const char *ns;
const char *selector;
} k8s_want_t;
/* Does label list `labels` ("k=v,k2=v2") carry `item` ("k=v") whole, or --
* `item` ending in "=" -- any value of that key? */
static bool k8s_labels_have(const char *labels, const char *item) {
size_t il = strlen(item);
bool any_value = il > 0 && item[il - SKIP_ONE] == '=';
for (const char *p = labels; p && *p;) {
size_t l = strcspn(p, ",");
if (any_value ? (l >= il && strncmp(p, item, il) == 0)
: (l == il && strncmp(p, item, il) == 0)) {
return true;
}
p = p[l] ? p + l + SKIP_ONE : NULL;
}
return false;
}
/* Does Resource `t` meet label selector `sel`: every term `k=v` / `k==v`
* (carried), `k!=v` (not carried), `k` (the key present), `!k` (absent)? A
* set-based term (`k in (a,b)`) names no key and meets nothing. */
static bool k8s_selector_meets(const cbm_gbuf_node_t *t, const char *sel) {
char *labels = k8s_node_prop_dup(t, "labels");
bool ok = true;
for (const char *p = sel; ok && p && *p;) {
size_t l = strcspn(p, ",");
char term[CBM_SZ_512];
snprintf(term, sizeof(term), "%.*s", (int)l, p);
char *s = term;
while (*s == ' ') {
s++;
}
for (char *e = s + strlen(s); e > s && e[-SKIP_ONE] == ' '; e--) {
e[-SKIP_ONE] = '\0';
}
char item[CBM_SZ_512];
char *eq = strstr(s, "!=");
if (eq) {
snprintf(item, sizeof(item), "%.*s=%s", (int)(eq - s), s, eq + PAIR_LEN);
ok = !k8s_labels_have(labels, item);
} else if ((eq = strchr(s, '='))) {
const char *v = eq[SKIP_ONE] == '=' ? eq + PAIR_LEN : eq + SKIP_ONE;
snprintf(item, sizeof(item), "%.*s=%s", (int)(eq - s), s, v);
ok = k8s_labels_have(labels, item);
} else if (s[0] == '!') {
snprintf(item, sizeof(item), "%s=", s + SKIP_ONE);
ok = !k8s_labels_have(labels, item);
} else {
snprintf(item, sizeof(item), "%s=", s);
ok = s[0] && k8s_labels_have(labels, item);
}
p = p[l] ? p + l + SKIP_ONE : NULL;
}
cbm_free(CBM_MEM_CLASS_OTHER, labels);
return ok;
}
/* Is Resource `t` what `w` names? A namespace meets the target's declared
* one or its effective one. */
static bool k8s_patch_meets(const cbm_gbuf_node_t *t, const k8s_want_t *w) {
@@ -3648,9 +3738,11 @@ static bool k8s_patch_meets(const cbm_gbuf_node_t *t, const k8s_want_t *w) {
char decl[K8S_LABEL_LEN];
k8s_declared_ns(t->qualified_name, decl, sizeof(decl));
k8s_effective_ns(t, prop, sizeof(prop));
return strcmp(w->ns, decl) == 0 || strcmp(w->ns, prop) == 0;
if (strcmp(w->ns, decl) != 0 && strcmp(w->ns, prop) != 0) {
return false;
}
}
return true;
return !w->selector || k8s_selector_meets(t, w->selector);
}
static void k8s_patch_edge(cbm_pipeline_ctx_t *ctx, int64_t src, int64_t tgt,
@@ -3724,7 +3816,7 @@ static int64_t k8s_file_of(cbm_pipeline_ctx_t *ctx, const char *path) {
* File `kust_file` -- to what it names. */
static void k8s_patch_in_build(const k8s_build_t *b, const k8s_patch_t *p, int64_t kust_file) {
const k8s_ids_t *docs = p->path ? cbm_ht_get(b->by_file, p->path) : NULL;
if (!p->kind && !p->name) {
if (!p->kind && !p->name && !p->selector) {
for (int d = 0; docs && d < docs->count; d++) {
const cbm_gbuf_node_t *doc = cbm_gbuf_find_by_id(b->ctx->gbuf, docs->items[d]);
char kind[K8S_LABEL_LEN];
@@ -3736,12 +3828,12 @@ static void k8s_patch_in_build(const k8s_build_t *b, const k8s_patch_t *p, int64
}
k8s_node_prop(doc, "group", group, sizeof(group));
k8s_declared_ns(doc->qualified_name, ns, sizeof(ns));
k8s_want_t w = {kind, group[0] ? group : NULL, NULL, name, ns[0] ? ns : NULL};
k8s_want_t w = {kind, group[0] ? group : NULL, NULL, name, ns[0] ? ns : NULL, NULL};
k8s_patch_targets(b, p, doc->id, &w);
}
return;
}
k8s_want_t w = {p->kind, p->group, p->version, p->name, p->ns};
k8s_want_t w = {p->kind, p->group, p->version, p->name, p->ns, p->selector};
if (docs && docs->count > 0) {
for (int d = 0; d < docs->count; d++) {
k8s_patch_targets(b, p, docs->items[d], &w);
+1 -1
View File
@@ -1497,7 +1497,7 @@ int cbm_pipeline_build_fresh_semantic_manifest(cbm_pipeline_t *p, const char *pr
* curl / wget HTTP_CALLS the Route of the URL it states (pass_shell.c).
* A kustomize patch OVERRIDEs the objects of its build it changes, its
* file an IMPORTS of the kustomization; a Resource keeps the version of
* its apiVersion; a kustomization DEPLOYS the charts it inflates
* its apiVersion and its labels; a kustomization DEPLOYS the charts it inflates
* (helmCharts) (extract_k8s.c, pass_k8s.c). An index built before
* holds none of them, so it rebuilds once on upgrade. */
enum { CBM_SEMANTIC_INDEX_VERSION = 9 };
+23 -6
View File
@@ -2585,7 +2585,8 @@ TEST(probe_env_address_connects_to_its_service) {
* @<target file>", then "patch imports <n>" (IMPORTS of patch files), "maps
* into patches <n>" (INFRA_MAPS into a patch file's documents) and "version
* <v>" (base/deploy.yaml's Resource), "odd version <v>" (other/odd.yaml's:
* none without one, missing without the Resource). */
* none without one, missing without the Resource), "labels <l>"
* (base/deploy.yaml's). */
static char *gpf_patches(const GpfFile *files, int nfiles) {
GpfProj lp;
cbm_store_t *store = gpf_index_files(&lp, files, nfiles);
@@ -2606,7 +2607,9 @@ static char *gpf_patches(const GpfFile *files, int nfiles) {
"label='Resource' AND file_path='base/deploy.yaml'), '-') || char(10) || "
"'odd version ' || coalesce((SELECT coalesce(json_extract(properties,'$.version'), "
"'none') FROM nodes WHERE project=?1 AND label='Resource' AND "
"file_path='other/odd.yaml'), 'missing')";
"file_path='other/odd.yaml'), 'missing') || char(10) || 'labels ' || "
"coalesce((SELECT json_extract(properties,'$.labels') FROM nodes WHERE project=?1 "
"AND label='Resource' AND file_path='base/deploy.yaml'), '-')";
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) {
@@ -2627,7 +2630,9 @@ static char *gpf_patches(const GpfFile *files, int nfiles) {
* component's the objects of the build including it; a kind alone every
* object of that kind. Never: an object outside the build, one the patch
* does not name (another name, group, version or namespace), a component
* nothing includes, a label selector (not evaluated). A patch file is an import of
* nothing includes. A label selector meets the objects whose metadata.labels
* carry its terms (`k=v`, `k`, `k!=v`; a Resource keeps them sorted). A
* patch file is an import of
* the kustomization, never a member of its build (a Service selects no patch
* document). A Resource keeps the version of its apiVersion; a bare group
* (`apiVersion: example.io`) has none. */
@@ -2637,7 +2642,8 @@ TEST(probe_kustomize_patches_override_their_base) {
{"base/cm.yaml",
"apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: ${APP}-config\n namespace: app\n"},
{"base/deploy.yaml",
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: web\n namespace: app\nspec:\n"
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: web\n namespace: app\n"
" labels:\n tier: front\n app: web\n note: a=b\nspec:\n"
" template:\n metadata:\n labels:\n app: web\n"},
{"base/svc.yaml", "apiVersion: v1\nkind: Service\nmetadata:\n name: web\n namespace: app\n"
"spec:\n selector:\n app: web\n"},
@@ -2653,6 +2659,10 @@ TEST(probe_kustomize_patches_override_their_base) {
"- path: ops-v2.yaml\n target:\n group: apps\n version: v2\n kind: Deployment\n"
"- path: ops-batch.yaml\n target:\n group: batch\n kind: Deployment\n"
"- path: sel.yaml\n target:\n kind: Deployment\n labelSelector: tier=none\n"
"- path: lab.yaml\n target:\n kind: Deployment\n labelSelector: app=web,tier\n"
"- path: neq.yaml\n target:\n kind: Deployment\n labelSelector: app!=web\n"
"- path: sel-only.yaml\n target:\n labelSelector: tier=front\n"
"- path: ann.yaml\n target:\n kind: Deployment\n annotationSelector: x=y\n"
"- path: cm-patch.yaml\n"
"- patch: |-\n apiVersion: apps/v1\n kind: Deployment\n metadata:\n"
" name: missing\n"},
@@ -2661,6 +2671,10 @@ TEST(probe_kustomize_patches_override_their_base) {
"apiVersion: v1\nkind: Service\nmetadata:\n name: web\n namespace: elsewhere\n"},
{"overlays/prod/all.yaml", "- op: add\n path: /metadata/labels/tier\n value: front\n"},
{"overlays/prod/sel.yaml", "- op: replace\n path: /spec/replicas\n value: 6\n"},
{"overlays/prod/lab.yaml", "- op: replace\n path: /spec/replicas\n value: 7\n"},
{"overlays/prod/neq.yaml", "- op: replace\n path: /spec/replicas\n value: 8\n"},
{"overlays/prod/sel-only.yaml", "- op: replace\n path: /spec/replicas\n value: 9\n"},
{"overlays/prod/ann.yaml", "- op: replace\n path: /spec/replicas\n value: 1\n"},
{"overlays/prod/cm-patch.yaml",
"apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: ${APP}-config\n namespace: app\n"
"data:\n k: v\n"},
@@ -2686,12 +2700,15 @@ TEST(probe_kustomize_patches_override_their_base) {
"overlays/prod/all.yaml File -> Service/web @base/svc.yaml\n"
"overlays/prod/cm-patch.yaml Resource -> ConfigMap/${APP}-config @base/cm.yaml\n"
"overlays/prod/kustomization.yaml File -> Service/web @base/svc.yaml\n"
"overlays/prod/lab.yaml File -> Deployment/web @base/deploy.yaml\n"
"overlays/prod/ops.yaml File -> Deployment/web @base/deploy.yaml\n"
"overlays/prod/patch-deploy.yaml Resource -> Deployment/web @base/deploy.yaml\n"
"patch imports 8\n"
"overlays/prod/sel-only.yaml File -> Deployment/web @base/deploy.yaml\n"
"patch imports 12\n"
"maps into patches 0\n"
"version v1\n"
"odd version none";
"odd version none\n"
"labels app=web,tier=front";
if (!got || strcmp(got, want) != 0) {
fprintf(stderr, "patches got:\n%s\nwant:\n%s\n", got ? got : "(null)", want);
free(got);