diff --git a/README.md b/README.md index 9ff713e0e..a393677dc 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index bf2965186..86586dd15 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -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; diff --git a/internal/cbm/extract_k8s.c b/internal/cbm/extract_k8s.c index 8a827c354..a3572763e 100644 --- a/internal/cbm/extract_k8s.c +++ b/internal/cbm/extract_k8s.c @@ -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); diff --git a/src/pipeline/pass_k8s.c b/src/pipeline/pass_k8s.c index 0182cddda..7ad950bad 100644 --- a/src/pipeline/pass_k8s.c +++ b/src/pipeline/pass_k8s.c @@ -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); diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index 0f6ed676a..92e20f8fb 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -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 }; diff --git a/tests/test_grammar_probe_f.c b/tests/test_grammar_probe_f.c index 05c45be25..e4e180798 100644 --- a/tests/test_grammar_probe_f.c +++ b/tests/test_grammar_probe_f.c @@ -2585,7 +2585,8 @@ TEST(probe_env_address_connects_to_its_service) { * @", then "patch imports " (IMPORTS of patch files), "maps * into patches " (INFRA_MAPS into a patch file's documents) and "version * " (base/deploy.yaml's Resource), "odd version " (other/odd.yaml's: - * none without one, missing without the Resource). */ + * none without one, missing without the Resource), "labels " + * (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);