diff --git a/README.md b/README.md index 5623f63fe..45e61a46d 100644 --- a/README.md +++ b/README.md @@ -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...`, 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 diff --git a/src/pipeline/pass_k8s.c b/src/pipeline/pass_k8s.c index 30de477e8..a8702948d 100644 --- a/src/pipeline/pass_k8s.c +++ b/src/pipeline/pass_k8s.c @@ -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); diff --git a/src/pipeline/pass_terraform.c b/src/pipeline/pass_terraform.c index 0eb47257c..6dad7172b 100644 --- a/src/pipeline/pass_terraform.c +++ b/src/pipeline/pass_terraform.c @@ -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_[_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: ::/ + * //), 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_[_vN[beta|alphaM]]": 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; diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index 0e6904044..22bcc85c5 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -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_ 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 diff --git a/tests/test_grammar_probe_f.c b/tests/test_grammar_probe_f.c index 7879a0ac9..9e0af6a09 100644 --- a/tests/test_grammar_probe_f.c +++ b/tests/test_grammar_probe_f.c @@ -3181,6 +3181,101 @@ TEST(probe_ansible_files_import_what_they_name) { PASS(); } +/* Every Resource a Terraform block DEPLOYS as " DEPLOYS + * v=", then "terraform resources ": the Resources + * marked ecosystem terraform, then each INFRA_MAPS edge into one of them as + * "