mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
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:
1 parent
35833e1f91
commit
e58c5ba730
6 files changed
+232
-45
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user