mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-26 06:43:15 +08:00
feat(infra): Kubernetes objects declared in Terraform are Resources
A `kubernetes_*` resource in a .tf file -- eks-blueprints' network
policies and storage classes, k8s.io's metrics-server stack -- declares
a cluster object, but the graph only had the Terraform block, named by
its Terraform address: no node carried the object's kind and name.
A kubernetes_<type> resource (also _vN) whose metadata name is a literal
is now a Resource of the same identity scheme as a manifest's object:
kind from the resource type, or a kubernetes_manifest's own apiVersion
and kind; name and namespace from the object's own metadata, never from
spec; `ecosystem: terraform` on its properties. The .tf file DEFINES it
and the block DEPLOYS it {via: terraform, line}. The objects are minted
before the Kubernetes joins, so a manifest of the same bundle references
them as it does a manifest's object (an Ingress maps to the Service). A
computed name, and a type that acts on an object declared elsewhere
(kubernetes_annotations, kubernetes_labels, ...), make none. Labels are
not read, so a Service selector does not reach a Terraform workload.
Measured on 17 infrastructure repositories: 21 of 21 literal-named
objects against an independent reading, with group, kind, name and
namespace; nothing else changed (21 nodes, 42 edges added, no property
of an existing node); deterministic over three runs; the incremental
re-index equals a full index when an object is renamed, gains a
namespace, or its file is added or deleted.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
@@ -252,7 +252,7 @@ The install script placed beside the binary is **reported, not deleted** — uni
|
||||
- `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 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 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, 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
|
||||
|
||||
+24
-7
@@ -554,10 +554,12 @@ static bool k8s_cluster_scoped(const char *kind) {
|
||||
* 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;
|
||||
* `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). */
|
||||
* nothing JSON escapes in it); `ecosystem` (NULL: a manifest's) what
|
||||
* declares it otherwise -- "terraform" (pass_terraform.c), plain text. 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) {
|
||||
const char *labels, const char *ecosystem) {
|
||||
enum { K8S_KEY_PARTS = 4, K8S_ESC = 6 * K8S_LABEL_LEN };
|
||||
const char *key = qn ? strstr(qn, "::") : NULL;
|
||||
if (!key) {
|
||||
@@ -597,8 +599,11 @@ static char *k8s_resource_props(const char *qn, const char *effective_ns, const
|
||||
if (!props) {
|
||||
return NULL;
|
||||
}
|
||||
int w =
|
||||
snprintf(props, n, "{\"source\":\"k8s\",\"group\":\"%s\",\"kind\":\"%s\"", esc[0], esc[1]);
|
||||
int w = snprintf(props, n, "{\"source\":\"k8s\"");
|
||||
if (ecosystem && ecosystem[0]) {
|
||||
w += snprintf(props + w, n - (size_t)w, ",\"ecosystem\":\"%s\"", ecosystem);
|
||||
}
|
||||
w += snprintf(props + w, n - (size_t)w, ",\"group\":\"%s\",\"kind\":\"%s\"", esc[0], esc[1]);
|
||||
if (esc[5][0]) {
|
||||
w += snprintf(props + w, n - (size_t)w, ",\"version\":\"%s\"", esc[5]);
|
||||
}
|
||||
@@ -613,6 +618,11 @@ static char *k8s_resource_props(const char *qn, const char *effective_ns, const
|
||||
return props;
|
||||
}
|
||||
|
||||
char *cbm_k8s_resource_props(const char *qn, const char *effective_ns, const char *version,
|
||||
const char *labels, const char *ecosystem) {
|
||||
return k8s_resource_props(qn, effective_ns, version, labels, ecosystem);
|
||||
}
|
||||
|
||||
/* The string property `key` of node `n` (as k8s_resource_props writes it:
|
||||
* no escapes inside) into `out`, "" when it has none. */
|
||||
static void k8s_node_prop(const cbm_gbuf_node_t *n, const char *key, char *out, size_t out_sz) {
|
||||
@@ -1171,9 +1181,12 @@ static void k8s_apply_effective_namespaces(cbm_pipeline_ctx_t *ctx, k8s_bundles_
|
||||
continue; /* no build sets a namespace: the declared one stands */
|
||||
}
|
||||
char version[K8S_LABEL_LEN];
|
||||
char ecosystem[K8S_LABEL_LEN];
|
||||
k8s_node_prop(n, "version", version, sizeof(version));
|
||||
k8s_node_prop(n, "ecosystem", ecosystem, sizeof(ecosystem));
|
||||
char *labels = k8s_node_prop_dup(n, "labels");
|
||||
char *props = k8s_resource_props(n->qualified_name, agree ? eff : "?", version, labels);
|
||||
char *props =
|
||||
k8s_resource_props(n->qualified_name, agree ? eff : "?", version, labels, ecosystem);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, labels);
|
||||
if (props) {
|
||||
(void)cbm_gbuf_node_set_properties_json((cbm_gbuf_node_t *)n, props);
|
||||
@@ -2427,7 +2440,7 @@ static void handle_k8s_manifest(cbm_pipeline_ctx_t *ctx, const char *rel_path, c
|
||||
labels = kv[SKIP_ONE];
|
||||
}
|
||||
}
|
||||
char *props = k8s_resource_props(def->qualified_name, NULL, version, labels);
|
||||
char *props = k8s_resource_props(def->qualified_name, NULL, version, labels, NULL);
|
||||
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\"}");
|
||||
@@ -4131,6 +4144,10 @@ int cbm_pipeline_pass_k8s(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
|
||||
}
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, prep);
|
||||
|
||||
/* Kubernetes objects Terraform declares (pass_terraform.c), before the
|
||||
* bundles and the joins: references meet them like manifest objects. */
|
||||
cbm_pipeline_tf_k8s_objects(ctx, files, file_count);
|
||||
|
||||
/* Connect Services to the workloads their selectors target (INFRA_MAPS),
|
||||
* inside the bundles that deploy them together. */
|
||||
k8s_bundles_build(ctx, &kusts, &bundles);
|
||||
|
||||
@@ -862,12 +862,12 @@ static tf_mod_t *tf_mod_parse(cbm_pipeline_ctx_t *ctx, CBMArena *arena, TSParser
|
||||
return m;
|
||||
}
|
||||
|
||||
/* Folder path -> its Folder node. */
|
||||
static CBMHashTable *tf_folder_index(cbm_pipeline_ctx_t *ctx) {
|
||||
/* path -> node, over the nodes of one label (Folder, File). */
|
||||
static CBMHashTable *tf_label_index(cbm_pipeline_ctx_t *ctx, const char *label) {
|
||||
CBMHashTable *ix = cbm_ht_create(0);
|
||||
const cbm_gbuf_node_t **nodes = NULL;
|
||||
int n = 0;
|
||||
if (ix && cbm_gbuf_find_by_label(ctx->gbuf, "Folder", &nodes, &n) == 0) {
|
||||
if (ix && cbm_gbuf_find_by_label(ctx->gbuf, label, &nodes, &n) == 0) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (nodes[i]->file_path) {
|
||||
cbm_ht_set(ix, nodes[i]->file_path, (void *)nodes[i]);
|
||||
@@ -976,7 +976,7 @@ static int tf_link_repo(cbm_pipeline_ctx_t *ctx, TSParser *parser, const char *c
|
||||
}
|
||||
i = j;
|
||||
}
|
||||
CBMHashTable *folders = calls > 0 ? tf_folder_index(ctx) : NULL;
|
||||
CBMHashTable *folders = calls > 0 ? tf_label_index(ctx, "Folder") : NULL;
|
||||
int edges = 0;
|
||||
for (int k = 0; k < nm; k++) {
|
||||
edges += tf_mod_link(list[k], mods, folders);
|
||||
@@ -1402,7 +1402,7 @@ static int tg_link_units(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
|
||||
}
|
||||
if (nu > 0 && ts_parser_set_language(parser, cbm_ts_language(CBM_LANG_HCL))) {
|
||||
qsort((void *)units, (size_t)nu, sizeof(*units), tg_cmp_str);
|
||||
g.folders = tf_folder_index(ctx);
|
||||
g.folders = tf_label_index(ctx, "Folder");
|
||||
for (int i = 0; i < nu; i++) {
|
||||
tg_unit(&g, parser, units[i]);
|
||||
}
|
||||
@@ -1418,6 +1418,291 @@ static int tg_link_units(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
|
||||
return g.edges;
|
||||
}
|
||||
|
||||
/* ── Kubernetes objects Terraform declares (PHASE 6 P6.5) ──────────── */
|
||||
|
||||
/* resource "kubernetes_<type>[_vN]" "N" declares the object of the kind its
|
||||
* type names -- a kubernetes_manifest the apiVersion / kind of its manifest
|
||||
* -- with metadata name and namespace from string literals. It is a
|
||||
* Resource of the K8s identity scheme (IDENTITY.md: <file-base>::<group>/
|
||||
* <Kind>/<ns or ->/<name>), with a manifest object's properties plus
|
||||
* `ecosystem: terraform`; its file DEFINES it and the block DEPLOYS
|
||||
* {via: terraform, line} it. A computed name (var., local., an
|
||||
* interpolation) is unknown: no Resource. Types acting on an object declared
|
||||
* elsewhere (owner references, readiness waits, label / annotation patches)
|
||||
* declare none. Run before the K8s joins: references meet these objects like
|
||||
* manifest ones. */
|
||||
|
||||
typedef struct {
|
||||
const char *type; /* after "kubernetes_", without a _vN suffix */
|
||||
const char *group;
|
||||
const char *kind;
|
||||
} tf_k8s_kind_t;
|
||||
|
||||
static const tf_k8s_kind_t TF_K8S_KINDS[] = {
|
||||
{"namespace", "core", "Namespace"},
|
||||
{"service", "core", "Service"},
|
||||
{"service_account", "core", "ServiceAccount"},
|
||||
{"default_service_account", "core", "ServiceAccount"},
|
||||
{"config_map", "core", "ConfigMap"},
|
||||
{"secret", "core", "Secret"},
|
||||
{"persistent_volume_claim", "core", "PersistentVolumeClaim"},
|
||||
{"persistent_volume", "core", "PersistentVolume"},
|
||||
{"pod", "core", "Pod"},
|
||||
{"limit_range", "core", "LimitRange"},
|
||||
{"resource_quota", "core", "ResourceQuota"},
|
||||
{"endpoints", "core", "Endpoints"},
|
||||
{"replication_controller", "core", "ReplicationController"},
|
||||
{"deployment", "apps", "Deployment"},
|
||||
{"stateful_set", "apps", "StatefulSet"},
|
||||
{"daemonset", "apps", "DaemonSet"},
|
||||
{"daemon_set", "apps", "DaemonSet"},
|
||||
{"job", "batch", "Job"},
|
||||
{"cron_job", "batch", "CronJob"},
|
||||
{"cluster_role", "rbac.authorization.k8s.io", "ClusterRole"},
|
||||
{"cluster_role_binding", "rbac.authorization.k8s.io", "ClusterRoleBinding"},
|
||||
{"role", "rbac.authorization.k8s.io", "Role"},
|
||||
{"role_binding", "rbac.authorization.k8s.io", "RoleBinding"},
|
||||
{"network_policy", "networking.k8s.io", "NetworkPolicy"},
|
||||
{"ingress", "networking.k8s.io", "Ingress"},
|
||||
{"ingress_class", "networking.k8s.io", "IngressClass"},
|
||||
{"storage_class", "storage.k8s.io", "StorageClass"},
|
||||
{"csi_driver", "storage.k8s.io", "CSIDriver"},
|
||||
{"priority_class", "scheduling.k8s.io", "PriorityClass"},
|
||||
{"horizontal_pod_autoscaler", "autoscaling", "HorizontalPodAutoscaler"},
|
||||
{"pod_disruption_budget", "policy", "PodDisruptionBudget"},
|
||||
{"runtime_class", "node.k8s.io", "RuntimeClass"},
|
||||
{"mutating_webhook_configuration", "admissionregistration.k8s.io",
|
||||
"MutatingWebhookConfiguration"},
|
||||
{"validating_webhook_configuration", "admissionregistration.k8s.io",
|
||||
"ValidatingWebhookConfiguration"},
|
||||
{"api_service", "apiregistration.k8s.io", "APIService"},
|
||||
{"certificate_signing_request", "certificates.k8s.io", "CertificateSigningRequest"},
|
||||
};
|
||||
|
||||
/* "kubernetes_<type>[_vN[beta|alphaM]]": <type> into out; false for another
|
||||
* provider's type. */
|
||||
static bool tf_k8s_type(const char *rtype, char *out, size_t sz) {
|
||||
static const char PREFIX[] = "kubernetes_";
|
||||
if (strncmp(rtype, PREFIX, sizeof(PREFIX) - SKIP_ONE) != 0) {
|
||||
return false;
|
||||
}
|
||||
snprintf(out, sz, "%s", rtype + sizeof(PREFIX) - SKIP_ONE);
|
||||
char *v = strrchr(out, '_');
|
||||
if (v && v[SKIP_ONE] == 'v' && v[PAIR_LEN] >= '0' && v[PAIR_LEN] <= '9') {
|
||||
*v = '\0';
|
||||
}
|
||||
return out[0] != '\0';
|
||||
}
|
||||
|
||||
/* A string literal's text (no interpolation, no ${path.module}); false else. */
|
||||
static bool tf_literal(const char *src, TSNode n, char *out, size_t sz) {
|
||||
return tf_string(src, "", n, out, sz) && !strchr(out, '\x01');
|
||||
}
|
||||
|
||||
/* The value of `key` in an object expression (keys bare or quoted). */
|
||||
static TSNode tf_object_get(const char *src, TSNode expr, const char *key) {
|
||||
TSNode n = expr;
|
||||
while ((tf_type(n, "expression") || tf_type(n, "collection_value")) &&
|
||||
ts_node_named_child_count(n) > 0) {
|
||||
n = ts_node_named_child(n, 0);
|
||||
}
|
||||
for (uint32_t i = 0; tf_type(n, "object") && i < ts_node_named_child_count(n); i++) {
|
||||
TSNode el = ts_node_named_child(n, i);
|
||||
if (!tf_type(el, "object_elem")) {
|
||||
continue;
|
||||
}
|
||||
TSNode k = ts_node_child_by_field_name(el, "key", sizeof("key") - SKIP_ONE);
|
||||
char text[CBM_SZ_64];
|
||||
bool named = tf_literal(src, k, text, sizeof(text));
|
||||
if (!named) {
|
||||
TSNode id = k;
|
||||
while ((tf_type(id, "expression") || tf_type(id, "variable_expr")) &&
|
||||
ts_node_named_child_count(id) > 0) {
|
||||
id = ts_node_named_child(id, 0);
|
||||
}
|
||||
named = tf_type(id, "identifier") && tf_text_is(src, id, key);
|
||||
text[0] = '\0';
|
||||
}
|
||||
if ((named && !text[0]) || (text[0] && strcmp(text, key) == 0)) {
|
||||
return ts_node_child_by_field_name(el, "val", sizeof("val") - SKIP_ONE);
|
||||
}
|
||||
}
|
||||
return (TSNode){0};
|
||||
}
|
||||
|
||||
/* The expression of attribute `key` in a block body, or a null node. */
|
||||
static TSNode tf_body_attr(const char *src, TSNode body, const char *key) {
|
||||
for (uint32_t i = 0; !ts_node_is_null(body) && i < ts_node_named_child_count(body); i++) {
|
||||
TSNode at = ts_node_named_child(body, i);
|
||||
if (tf_type(at, "attribute") && ts_node_named_child_count(at) >= PAIR_LEN &&
|
||||
tf_text_is(src, ts_node_named_child(at, 0), key)) {
|
||||
return ts_node_named_child(at, SKIP_ONE);
|
||||
}
|
||||
}
|
||||
return (TSNode){0};
|
||||
}
|
||||
|
||||
/* The body of the nested block `name` in a block body, or a null node. */
|
||||
static TSNode tf_body_block(const char *src, TSNode body, const char *name) {
|
||||
for (uint32_t i = 0; !ts_node_is_null(body) && i < ts_node_named_child_count(body); i++) {
|
||||
TSNode b = ts_node_named_child(body, i);
|
||||
if (!tf_type(b, "block") || ts_node_named_child_count(b) == 0 ||
|
||||
!tf_text_is(src, ts_node_named_child(b, 0), name)) {
|
||||
continue;
|
||||
}
|
||||
for (uint32_t j = 0; j < ts_node_named_child_count(b); j++) {
|
||||
if (tf_type(ts_node_named_child(b, j), "body")) {
|
||||
return ts_node_named_child(b, j);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (TSNode){0};
|
||||
}
|
||||
|
||||
/* One top-level block of a .tf file: the object it declares, if any. */
|
||||
static void tf_k8s_block(cbm_pipeline_ctx_t *ctx, CBMArena *a, const char *src, const char *rel,
|
||||
const char *base, int64_t file_id, TSNode block) {
|
||||
TSNode body;
|
||||
const char *bname = tf_block_name(a, src, block, &body);
|
||||
static const char RES[] = "resource.";
|
||||
char rtype[CBM_SZ_128];
|
||||
char type[CBM_SZ_128];
|
||||
if (!bname || ts_node_is_null(body) || strncmp(bname, RES, sizeof(RES) - SKIP_ONE) != 0) {
|
||||
return;
|
||||
}
|
||||
const char *dot = strchr(bname + sizeof(RES) - SKIP_ONE, '.');
|
||||
snprintf(rtype, sizeof(rtype), "%.*s", dot ? (int)(dot - bname - (sizeof(RES) - SKIP_ONE)) : 0,
|
||||
bname + sizeof(RES) - SKIP_ONE);
|
||||
if (!tf_k8s_type(rtype, type, sizeof(type))) {
|
||||
return;
|
||||
}
|
||||
char group[CBM_SZ_256] = "";
|
||||
char kind[CBM_SZ_128] = "";
|
||||
char version[CBM_SZ_64] = "";
|
||||
char name[CBM_SZ_256] = "";
|
||||
char ns[CBM_SZ_256] = "";
|
||||
if (strcmp(type, "manifest") == 0) {
|
||||
TSNode m = tf_body_attr(src, body, "manifest");
|
||||
char av[CBM_SZ_256];
|
||||
TSNode meta = tf_object_get(src, m, "metadata");
|
||||
if (!tf_literal(src, tf_object_get(src, m, "kind"), kind, sizeof(kind)) ||
|
||||
!tf_literal(src, tf_object_get(src, m, "apiVersion"), av, sizeof(av)) ||
|
||||
!tf_literal(src, tf_object_get(src, meta, "name"), name, sizeof(name))) {
|
||||
return;
|
||||
}
|
||||
const char *slash = strchr(av, '/');
|
||||
if (slash) {
|
||||
snprintf(group, sizeof(group), "%.*s", (int)(slash - av), av);
|
||||
} else {
|
||||
snprintf(group, sizeof(group), "core");
|
||||
}
|
||||
snprintf(version, sizeof(version), "%s", slash ? slash + SKIP_ONE : av);
|
||||
if (!tf_literal(src, tf_object_get(src, meta, "namespace"), ns, sizeof(ns))) {
|
||||
ns[0] = '\0';
|
||||
}
|
||||
} else {
|
||||
const tf_k8s_kind_t *k = NULL;
|
||||
for (size_t i = 0; i < sizeof(TF_K8S_KINDS) / sizeof(TF_K8S_KINDS[0]) && !k; i++) {
|
||||
k = strcmp(TF_K8S_KINDS[i].type, type) == 0 ? &TF_K8S_KINDS[i] : NULL;
|
||||
}
|
||||
TSNode meta = tf_body_block(src, body, "metadata");
|
||||
if (!k) {
|
||||
return;
|
||||
}
|
||||
snprintf(group, sizeof(group), "%s", k->group);
|
||||
snprintf(kind, sizeof(kind), "%s", k->kind);
|
||||
if (strcmp(type, "default_service_account") == 0) {
|
||||
snprintf(name, sizeof(name), "default");
|
||||
} else if (!tf_literal(src, tf_body_attr(src, meta, "name"), name, sizeof(name))) {
|
||||
return;
|
||||
}
|
||||
if (!tf_literal(src, tf_body_attr(src, meta, "namespace"), ns, sizeof(ns))) {
|
||||
ns[0] = '\0';
|
||||
}
|
||||
}
|
||||
if (strchr(name, '/') || strchr(ns, '/') || strchr(kind, '/') || !name[0] || !kind[0]) {
|
||||
return; /* the identity key splits on "/" */
|
||||
}
|
||||
const char *qn =
|
||||
cbm_arena_sprintf(a, "%s::%s/%s/%s/%s", base, group, kind, ns[0] ? ns : "-", name);
|
||||
const char *rname = cbm_arena_sprintf(a, "%s/%s", kind, name);
|
||||
char *props = qn ? cbm_k8s_resource_props(qn, NULL, version, NULL, "terraform") : NULL;
|
||||
int64_t id = props ? cbm_gbuf_upsert_node(ctx->gbuf, "Resource", rname, qn, rel,
|
||||
(int)ts_node_start_point(block).row + SKIP_ONE,
|
||||
(int)ts_node_end_point(block).row + SKIP_ONE, props)
|
||||
: 0;
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, props);
|
||||
if (id <= 0) {
|
||||
return;
|
||||
}
|
||||
if (file_id > 0) {
|
||||
cbm_gbuf_insert_edge(ctx->gbuf, file_id, id, "DEFINES", "{}");
|
||||
}
|
||||
const char *bqn = cbm_fqn_compute(a, ctx->project_name, rel, bname);
|
||||
const cbm_gbuf_node_t *bnode = bqn ? cbm_gbuf_find_by_qn(ctx->gbuf, bqn) : NULL;
|
||||
if (bnode && bnode->id != id) {
|
||||
char eprops[CBM_SZ_64];
|
||||
snprintf(eprops, sizeof(eprops), "{\"via\":\"terraform\",\"line\":%d}",
|
||||
(int)ts_node_start_point(block).row + SKIP_ONE);
|
||||
cbm_gbuf_insert_edge(ctx->gbuf, bnode->id, id, "DEPLOYS", eprops);
|
||||
}
|
||||
}
|
||||
|
||||
void cbm_pipeline_tf_k8s_objects(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
|
||||
int file_count) {
|
||||
TSParser *parser = NULL;
|
||||
CBMHashTable *file_nodes = NULL;
|
||||
for (int i = 0; i < file_count; i++) {
|
||||
const char *rel = files[i].rel_path;
|
||||
if (files[i].language != CBM_LANG_HCL || !tf_is_module_file(rel)) {
|
||||
continue;
|
||||
}
|
||||
size_t len = 0;
|
||||
char *src = tf_read(files[i].path, &len);
|
||||
if (!src || !strstr(src, "kubernetes_")) {
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, src);
|
||||
continue;
|
||||
}
|
||||
if (!parser) {
|
||||
parser = ts_parser_new();
|
||||
file_nodes = tf_label_index(ctx, "File");
|
||||
if (!parser || !ts_parser_set_language(parser, cbm_ts_language(CBM_LANG_HCL))) {
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, src);
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* the file base: the File node's QN without ".__file__" (IDENTITY.md) */
|
||||
static const char FILE_SUFFIX[] = ".__file__";
|
||||
const cbm_gbuf_node_t *file = file_nodes ? cbm_ht_get(file_nodes, rel) : NULL;
|
||||
size_t fl = file && file->qualified_name ? strlen(file->qualified_name) : 0;
|
||||
size_t sl = sizeof(FILE_SUFFIX) - SKIP_ONE;
|
||||
CBMArena arena;
|
||||
cbm_arena_init_lazy(&arena, CBM_SZ_4K);
|
||||
const char *base = fl > sl && strcmp(file->qualified_name + fl - sl, FILE_SUFFIX) == 0
|
||||
? cbm_arena_strndup(&arena, file->qualified_name, fl - sl)
|
||||
: NULL;
|
||||
TSTree *tree = base ? ts_parser_parse_string(parser, NULL, src, (uint32_t)len) : NULL;
|
||||
TSNode body = tree ? tf_root_body(ts_tree_root_node(tree)) : (TSNode){0};
|
||||
for (uint32_t j = 0; !ts_node_is_null(body) && j < ts_node_named_child_count(body); j++) {
|
||||
TSNode b = ts_node_named_child(body, j);
|
||||
if (tf_type(b, "block")) {
|
||||
tf_k8s_block(ctx, &arena, src, rel, base, file->id, b);
|
||||
}
|
||||
}
|
||||
cbm_arena_destroy(&arena);
|
||||
if (tree) {
|
||||
ts_tree_delete(tree);
|
||||
}
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, src);
|
||||
}
|
||||
if (parser) {
|
||||
ts_parser_delete(parser);
|
||||
}
|
||||
if (file_nodes) {
|
||||
cbm_ht_free(file_nodes);
|
||||
}
|
||||
}
|
||||
|
||||
void cbm_pipeline_pass_terraform(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
|
||||
int file_count) {
|
||||
TSParser *parser = NULL;
|
||||
|
||||
@@ -1304,6 +1304,17 @@ void cbm_build_sites(CBMArena *a, const char *rel, const char *src, size_t len,
|
||||
void cbm_pipeline_pass_terraform(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
|
||||
int file_count);
|
||||
|
||||
/* Kubernetes objects Terraform declares (pass_terraform.c): a
|
||||
* kubernetes_<type> resource with a literal name is a Resource of the K8s
|
||||
* identity scheme, ecosystem terraform. Run by the K8s pass before its joins. */
|
||||
void cbm_pipeline_tf_k8s_objects(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
|
||||
int file_count);
|
||||
|
||||
/* A Resource's properties from its identity QN (pass_k8s.c,
|
||||
* k8s_resource_props); `ecosystem` NULL for a manifest's. Caller frees. */
|
||||
char *cbm_k8s_resource_props(const char *qn, const char *effective_ns, const char *version,
|
||||
const char *labels, const char *ecosystem);
|
||||
|
||||
/* Ansible (pass_ansible.c): a playbook or task file IMPORTS the playbooks,
|
||||
* task files and roles its directives name. Run at the end of the K8s pass,
|
||||
* over the same files. */
|
||||
@@ -1520,7 +1531,9 @@ int cbm_pipeline_build_fresh_semantic_manifest(cbm_pipeline_t *p, const char *pr
|
||||
* {via:tfvars} the variable block of its own directory (or, inferred,
|
||||
* of the nearest ancestor module declaring its names); a Terragrunt
|
||||
* unit's include / read_terragrunt_config IMPORTS {via:terragrunt} the
|
||||
* file it names, a dependency DEPENDS_ON the other unit
|
||||
* file it names, a dependency DEPENDS_ON the other unit; a
|
||||
* kubernetes_* resource with a literal name is a Resource
|
||||
* {ecosystem:terraform} its block DEPLOYS
|
||||
* (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 (pass_ansible.c). An index built
|
||||
|
||||
@@ -3181,6 +3181,101 @@ TEST(probe_ansible_files_import_what_they_name) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Every Resource a Terraform block DEPLOYS as "<block> DEPLOYS <name>
|
||||
* <obj_key> v=<version>", then "terraform resources <n>": the Resources
|
||||
* marked ecosystem terraform, then each INFRA_MAPS edge into one of them as
|
||||
* "<label>:<name> INFRA_MAPS <name>". */
|
||||
static char *gpf_tf_k8s(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.name || ' ' || json_extract(t.properties,'$.obj_key') || ' v=' || "
|
||||
"coalesce(json_extract(t.properties,'$.version'), '-') 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 t.label='Resource' AND "
|
||||
"json_extract(e.properties,'$.via')='terraform' ORDER BY x)), '') || char(10) || "
|
||||
"'terraform resources ' || (SELECT COUNT(*) FROM nodes WHERE project=?1 AND "
|
||||
"label='Resource' AND json_extract(properties,'$.ecosystem')='terraform') || "
|
||||
"coalesce((SELECT group_concat(y, '') FROM (SELECT char(10) || s.label || ':' || s.name "
|
||||
"|| ' INFRA_MAPS ' || t.name AS y 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='INFRA_MAPS' AND "
|
||||
"json_extract(t.properties,'$.ecosystem')='terraform' ORDER BY y)), '')";
|
||||
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 kubernetes_<type> resource (also _vN) with a literal metadata name is a
|
||||
* Resource of the K8s identity scheme, ecosystem terraform, its block
|
||||
* DEPLOYS it: the kind its type names, or a kubernetes_manifest's own
|
||||
* apiVersion / kind. The name and namespace come from the object's own
|
||||
* metadata only (never spec's), a default service account's is `default`;
|
||||
* a cluster-scoped kind's obj_key says `_`. A
|
||||
* computed name, and a type acting on an object declared elsewhere
|
||||
* (kubernetes_annotations), make none. A manifest of the same bundle
|
||||
* references it like a manifest's object: the Ingress maps to the Service. */
|
||||
TEST(probe_terraform_kubernetes_objects_are_resources) {
|
||||
static const GpfFile files[] = {
|
||||
{"k8s/main.tf",
|
||||
"resource \"kubernetes_namespace_v1\" \"apps\" {\n metadata {\n name = \"apps\"\n }\n}\n"
|
||||
"resource \"kubernetes_deployment\" \"web\" {\n metadata {\n name = \"web\"\n"
|
||||
" namespace = \"apps\"\n }\n spec {\n template {\n metadata {\n"
|
||||
" name = \"inner\"\n }\n }\n }\n}\n"
|
||||
"resource \"kubernetes_manifest\" \"cert\" {\n manifest = {\n"
|
||||
" \"apiVersion\" = \"cert-manager.io/v1\"\n \"kind\" = \"Certificate\"\n"
|
||||
" \"metadata\" = {\n \"name\" = \"tls\"\n \"namespace\" = \"apps\"\n"
|
||||
" }\n }\n}\n"
|
||||
"resource \"kubernetes_service\" \"computed\" {\n metadata {\n name = var.name\n }\n}\n"
|
||||
"resource \"kubernetes_api_service\" \"metrics\" {\n metadata {\n"
|
||||
" name = \"v1beta1.metrics.k8s.io\"\n }\n spec {\n service {\n"
|
||||
" name = \"metrics-server\"\n namespace = \"kube-system\"\n }\n }\n}\n"
|
||||
"resource \"kubernetes_annotations\" \"patch\" {\n api_version = \"v1\"\n"
|
||||
" kind = \"ConfigMap\"\n metadata {\n name = \"cm\"\n }\n}\n"
|
||||
"resource \"kubernetes_default_service_account\" \"sa\" {\n metadata {\n"
|
||||
" namespace = \"apps\"\n }\n}\n"
|
||||
"resource \"kubernetes_service_v1\" \"web\" {\n metadata {\n name = \"web\"\n"
|
||||
" namespace = \"apps\"\n }\n}\n"
|
||||
"variable \"name\" {}\n"},
|
||||
{"k8s/ingress.yaml",
|
||||
"apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n name: web\n"
|
||||
" namespace: apps\nspec:\n rules:\n - http:\n paths:\n - path: /\n"
|
||||
" pathType: Prefix\n backend:\n service:\n"
|
||||
" name: web\n port:\n number: 80\n"}};
|
||||
int n = (int)(sizeof(files) / sizeof(files[0]));
|
||||
char *got = gpf_tf_k8s(files, n);
|
||||
static const char want[] =
|
||||
"resource.kubernetes_api_service.metrics DEPLOYS APIService/v1beta1.metrics.k8s.io "
|
||||
"apiregistration.k8s.io/APIService/_/v1beta1.metrics.k8s.io v=-\n"
|
||||
"resource.kubernetes_default_service_account.sa DEPLOYS ServiceAccount/default "
|
||||
"core/ServiceAccount/apps/default v=-\n"
|
||||
"resource.kubernetes_deployment.web DEPLOYS Deployment/web apps/Deployment/apps/web v=-\n"
|
||||
"resource.kubernetes_manifest.cert DEPLOYS Certificate/tls "
|
||||
"cert-manager.io/Certificate/apps/tls v=v1\n"
|
||||
"resource.kubernetes_namespace_v1.apps DEPLOYS Namespace/apps core/Namespace/_/apps v=-\n"
|
||||
"resource.kubernetes_service_v1.web DEPLOYS Service/web core/Service/apps/web v=-\n"
|
||||
"terraform resources 6\n"
|
||||
"Resource:Ingress/web INFRA_MAPS Service/web\n"
|
||||
"Route:*/ INFRA_MAPS Service/web";
|
||||
if (!got || strcmp(got, want) != 0) {
|
||||
fprintf(stderr, "terraform k8s got:\n%s\nwant:\n%s\n", got ? got : "(null)", want);
|
||||
free(got);
|
||||
FAIL("terraform k8s");
|
||||
}
|
||||
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) {
|
||||
@@ -5624,6 +5719,7 @@ SUITE(grammar_probe_f) {
|
||||
RUN_TEST(probe_terraform_tfvars_override_their_variables);
|
||||
RUN_TEST(probe_terragrunt_units_compose_their_files);
|
||||
RUN_TEST(probe_ansible_files_import_what_they_name);
|
||||
RUN_TEST(probe_terraform_kubernetes_objects_are_resources);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user