Skip to content

Commit dec56c9

Browse files
committed
feat(infra): an external chart is keyed by the URL that publishes it
An external Chart was <project>.__helm_dep__.<name>: keyed by its name, inside one project. Two repositories naming the same published chart held two unrelated nodes, and a project naming two charts of one name from two publishers (kubermatic's cert-manager from charts.jetstack.io and from its own mirror) would have merged them into one. An external Chart whose repository URL is stated is now __chart__<repository>/<name>, the same QN in every project, the URL in a `repository` property: scheme and host lower-cased, a trailing slash and `.git` dropped. A Chart.yaml dependency, a kustomize helmCharts entry, a Flux HelmRelease chartRef and a Terraform helm_release of an oci:// chart that name one published chart meet in one node. A chart reference (a Flux OCIRepository url, `chart = "oci://..."`) is cut to its repository; a repository URL never is, so repository oci://public.ecr.aws/karpenter with chart karpenter stays oci://public.ecr.aws/karpenter/karpenter. A repository that is no URL (an @alias, no scheme, file://, credentials in the authority, an unexpanded ${...}) or that the pass does not read (a Flux HelmRepository source, a Terraform `repository =`, a shell `repo/name`) keeps the per-project node: no identity is claimed for what was not stated. A query or fragment (`?token=...`) takes the per-project node too, so a token never becomes part of a QN or a property. One predicate now tells an external chart from a chart of the repository where Terraform, shell and the Go installer index the chart directories: an external chart a Chart.yaml depends on carries that file's path, and must never stand in for the chart the directory declares. Measured on 17 infrastructure repositories against an independent reading: 141 of 141 edges into external charts reach the identity their stated repository gives (114 by URL, 27 per project). The only differences to the previous index are the 79 external Chart nodes that moved to their URL identity, each with its repository property, and the 114 edges into them, re-pointed with type, source and properties unchanged. Every chart gate (Helm includes, helmCharts, Terraform, the Go installer, shell, CI, GitOps deploys) reads the same as before; three runs are identical; the incremental re-index equals a full index for a shared chart's owning file edited and deleted, a URL respelled, a dependency moved to another publisher and to an alias, a chartRef URL edit and a new Terraform oci:// release. The index rebuilds once on upgrade with the other version 9 changes. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1 parent 150851c commit dec56c9

7 files changed

Lines changed: 351 additions & 60 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ The install script placed beside the binary is **reported, not deleted**: uninst
333333
- `INFRA_MAPS`: a Kubernetes Service to each workload whose pod labels carry every `key: value` of the Service's selector, within one namespace
334334
- `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
335335
- `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
336-
- `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`, ...)
336+
- `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 an external `Chart`, keyed by its publisher when its repository URL is stated (`__chart__<repository>/<name>`, the same node in every project, the URL in `repository`), else one per project and name; the edge says `via` (`flux`, `argo`, `helmrelease`, `kubectl`, `kustomize`, `helm`, `helm-sdk`, `terraform`, `helmcharts`, ...)
337337
- `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
338338
- `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`
339339
- `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

‎src/pipeline/pass_k8s.c‎

Lines changed: 177 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include "foundation/str_util.h" /* cbm_json_escape */
2727
#include "pipeline/worker_pool.h"
2828

29+
#include <ctype.h>
2930
#include <stdatomic.h>
3031
#include <stdlib.h>
3132
#include <string.h>
@@ -2542,14 +2543,159 @@ static char *k8s_dep_props(const char *repository) {
25422543
return props;
25432544
}
25442545

2545-
/* DEPENDS_ON from `chart_id` to the shared external Chart node of `name`
2546-
* (one per name and project, so charts depending on the same chart meet). */
2546+
/* ── External charts ─────────────────────────────────────────────── */
2547+
2548+
/* An external Chart is keyed by its publisher: `__chart__<url>/<name>`, the
2549+
* same QN in every project, so two repositories naming one published chart
2550+
* name one node. A chart whose repository does not resolve to a URL (a
2551+
* `@alias`, an empty or templated repository, a source the pass cannot read)
2552+
* keeps the per-project `<project>.__helm_dep__.<name>`: no identity is
2553+
* claimed for what was not stated. */
2554+
static const char K8S_CHART_QN_PREFIX[] = "__chart__";
2555+
static const char K8S_HELM_DEP_MARK[] = ".__helm_dep__.";
2556+
2557+
/* A byte a stated repository URL may hold: no whitespace or control byte,
2558+
* no quote, template or shell expansion, escape or angle bracket. */
2559+
static bool k8s_url_byte_ok(unsigned char c) {
2560+
return c > ' ' && c < 0x7f && !strchr("\"'`{}$<>\\", c);
2561+
}
2562+
2563+
bool cbm_chart_repo_url(const char *url, char *out, size_t sz) {
2564+
if (!url || !out || sz == 0) {
2565+
return false;
2566+
}
2567+
while (*url == ' ' || *url == '\t') {
2568+
url++;
2569+
}
2570+
size_t n = strlen(url);
2571+
while (n > 0 && isspace((unsigned char)url[n - SKIP_ONE])) {
2572+
n--;
2573+
}
2574+
if (n == 0 || n + SKIP_ONE > sz || !isalpha((unsigned char)url[0])) {
2575+
return false;
2576+
}
2577+
for (size_t i = 0; i < n; i++) {
2578+
if (!k8s_url_byte_ok((unsigned char)url[i])) {
2579+
return false;
2580+
}
2581+
}
2582+
/* a query or fragment may carry a token (?token=...): like credentials
2583+
* in the authority, never put into a QN or a property */
2584+
if (memchr(url, '?', n) || memchr(url, '#', n)) {
2585+
return false;
2586+
}
2587+
size_t s = 0; /* the scheme: [A-Za-z][A-Za-z0-9+.-]* then :// */
2588+
while (s < n &&
2589+
(isalnum((unsigned char)url[s]) || url[s] == '+' || url[s] == '-' || url[s] == '.')) {
2590+
s++;
2591+
}
2592+
if (s + K8S_SLEN("://") > n || strncmp(url + s, "://", K8S_SLEN("://")) != 0) {
2593+
return false;
2594+
}
2595+
size_t host = s + K8S_SLEN("://");
2596+
size_t path = host;
2597+
while (path < n && url[path] != '/') {
2598+
path++;
2599+
}
2600+
/* no host, or credentials in the authority: never put into a QN */
2601+
if (path == host || memchr(url + host, '@', path - host)) {
2602+
return false;
2603+
}
2604+
for (size_t i = 0; i < path; i++) {
2605+
out[i] = (char)tolower((unsigned char)url[i]);
2606+
}
2607+
memcpy(out + path, url + path, n - path);
2608+
out[n] = '\0';
2609+
if (strncmp(out, "file://", K8S_SLEN("file://")) == 0) {
2610+
return false; /* a directory, not a publisher */
2611+
}
2612+
size_t end = n;
2613+
while (end > path && out[end - SKIP_ONE] == '/') {
2614+
end--;
2615+
}
2616+
if (end - path >= K8S_SLEN(".git") &&
2617+
strncmp(out + end - K8S_SLEN(".git"), ".git", K8S_SLEN(".git")) == 0) {
2618+
end -= K8S_SLEN(".git");
2619+
while (end > path && out[end - SKIP_ONE] == '/') {
2620+
end--;
2621+
}
2622+
}
2623+
out[end] = '\0';
2624+
return true;
2625+
}
2626+
2627+
bool cbm_chart_ref_repo(const char *ref, char *out, size_t sz) {
2628+
if (!ref || !out || sz == 0) {
2629+
return false;
2630+
}
2631+
size_t end = strlen(ref);
2632+
while (end > 0 && ref[end - SKIP_ONE] == '/') {
2633+
end--;
2634+
}
2635+
while (end > 0 && ref[end - SKIP_ONE] != '/') {
2636+
end--;
2637+
}
2638+
while (end > 0 && ref[end - SKIP_ONE] == '/') {
2639+
end--;
2640+
}
2641+
if (end == 0 || end + SKIP_ONE > sz) {
2642+
return false;
2643+
}
2644+
memcpy(out, ref, end);
2645+
out[end] = '\0';
2646+
return true;
2647+
}
2648+
2649+
/* QN and node properties of the external Chart `name` (non-NULL) of
2650+
* repository `url` (NULL: unknown). */
2651+
static void k8s_chart_identity(const char *project, const char *name, const char *url, char *qn,
2652+
size_t qsz, char *props, size_t psz) {
2653+
char norm[CBM_SZ_1K];
2654+
if (name[0] && !strchr(name, '/') && cbm_chart_repo_url(url, norm, sizeof(norm))) {
2655+
int w = snprintf(qn, qsz, "%s%s/%s", K8S_CHART_QN_PREFIX, norm, name);
2656+
if (w > 0 && (size_t)w < qsz) {
2657+
char esc[CBM_SZ_2K];
2658+
cbm_json_escape(esc, (int)sizeof(esc), norm);
2659+
snprintf(props, psz, "{\"source\":\"helm\",\"external\":true,\"repository\":\"%s\"}",
2660+
esc);
2661+
return;
2662+
}
2663+
}
2664+
snprintf(qn, qsz, "%s%s%s", project, K8S_HELM_DEP_MARK, name);
2665+
snprintf(props, psz, "{\"source\":\"helm\",\"external\":true}");
2666+
}
2667+
2668+
int64_t cbm_pipeline_external_chart(cbm_pipeline_ctx_t *ctx, const char *name, const char *url,
2669+
const char *file) {
2670+
if (!name) {
2671+
return 0;
2672+
}
2673+
char qn[CBM_SZ_2K];
2674+
char props[CBM_SZ_4K];
2675+
k8s_chart_identity(ctx->project_name, name, url, qn, sizeof(qn), props, sizeof(props));
2676+
const cbm_gbuf_node_t *known = cbm_gbuf_find_by_qn(ctx->gbuf, qn);
2677+
if (known) {
2678+
return known->id;
2679+
}
2680+
return cbm_gbuf_upsert_node(ctx->gbuf, "Chart", name, qn, file, SKIP_ONE, 0, props);
2681+
}
2682+
2683+
bool cbm_pipeline_chart_is_external(const cbm_gbuf_node_t *node) {
2684+
const char *qn = node ? node->qualified_name : NULL;
2685+
return qn && (strncmp(qn, K8S_CHART_QN_PREFIX, K8S_SLEN(K8S_CHART_QN_PREFIX)) == 0 ||
2686+
strstr(qn, K8S_HELM_DEP_MARK) != NULL);
2687+
}
2688+
2689+
/* DEPENDS_ON from `chart_id` to the external Chart node of `name` in
2690+
* `repository` (charts depending on the same published chart meet). */
25472691
static bool k8s_external_dep(cbm_pipeline_ctx_t *ctx, int64_t chart_id, const char *name,
2548-
const char *chart_rel, const char *props) {
2549-
char dep_qn[CBM_SZ_512];
2550-
snprintf(dep_qn, sizeof(dep_qn), "%s.__helm_dep__.%s", ctx->project_name, name);
2551-
int64_t dep_id = cbm_gbuf_upsert_node(ctx->gbuf, "Chart", name, dep_qn, chart_rel, SKIP_ONE, 0,
2552-
"{\"source\":\"helm\",\"external\":true}");
2692+
const char *repository, const char *chart_rel, const char *props) {
2693+
char dep_qn[CBM_SZ_2K];
2694+
char node_props[CBM_SZ_4K];
2695+
k8s_chart_identity(ctx->project_name, name, repository, dep_qn, sizeof(dep_qn), node_props,
2696+
sizeof(node_props));
2697+
int64_t dep_id =
2698+
cbm_gbuf_upsert_node(ctx->gbuf, "Chart", name, dep_qn, chart_rel, SKIP_ONE, 0, node_props);
25532699
if (dep_id <= 0) {
25542700
return false;
25552701
}
@@ -2613,16 +2759,19 @@ static void k8s_link_local_deps(cbm_pipeline_ctx_t *ctx, k8s_local_deps_t *deps)
26132759
target->id != d->chart_id) {
26142760
cbm_gbuf_insert_edge(ctx->gbuf, d->chart_id, target->id, "DEPENDS_ON", d->props);
26152761
} else if (d->chart_rel) {
2616-
(void)k8s_external_dep(ctx, d->chart_id, d->name, d->chart_rel, d->props);
2762+
/* a file:// directory this repository cannot resolve names no
2763+
* publisher: the per-project external Chart */
2764+
(void)k8s_external_dep(ctx, d->chart_id, d->name, NULL, d->chart_rel, d->props);
26172765
}
26182766
}
26192767
k8s_local_deps_free(deps);
26202768
}
26212769

26222770
/* Emit a Chart node for a Chart.yaml and a DEPENDS_ON edge per declared
26232771
* dependency (#338): to the chart in this repository for a resolvable
2624-
* `file://` dependency (once all charts are read), otherwise to a shared
2625-
* external Chart node per dependency name, the repository on the edge. */
2772+
* `file://` dependency (once all charts are read), otherwise to the external
2773+
* Chart node of the dependency's repository and name, the repository also on
2774+
* the edge. */
26262775
static void handle_helm_chart(cbm_pipeline_ctx_t *ctx, const char *rel_path, const char *source,
26272776
k8s_local_deps_t *local_deps) {
26282777
cbm_helm_chart_t hc;
@@ -2656,7 +2805,7 @@ static void handle_helm_chart(cbm_pipeline_ctx_t *ctx, const char *rel_path, con
26562805
continue;
26572806
}
26582807
char *props = dep->repository[0] ? k8s_dep_props(dep->repository) : NULL;
2659-
dep_edges += k8s_external_dep(ctx, chart_id, dep->name, rel_path, props);
2808+
dep_edges += k8s_external_dep(ctx, chart_id, dep->name, dep->repository, rel_path, props);
26602809
cbm_free(CBM_MEM_CLASS_OTHER, props);
26612810
}
26622811
cbm_helm_chart_free(&hc);
@@ -3439,7 +3588,7 @@ static void k8s_link_includes(cbm_pipeline_ctx_t *ctx, k8s_bundles_t *b,
34393588
* outermost kustomizations below it -- their Kustomization node -- and the
34403589
* manifests outside any, their File node; a chart dir: its Chart); a
34413590
* HelmRelease -> its chart (a repository chart dir of a Git/Bucket source,
3442-
* else the shared external Chart of that name). */
3591+
* else the external Chart of that name). */
34433592

34443593
typedef struct {
34453594
cbm_pipeline_ctx_t *ctx;
@@ -3475,21 +3624,17 @@ static int64_t k8s_chart_node(k8s_dep_link_t *l, const char *dir) {
34753624
return 0;
34763625
}
34773626

3478-
/* The shared external Chart of `name` (the one chart dependencies use). */
3479-
static int64_t k8s_external_chart(k8s_dep_link_t *l, const char *name, const char *file) {
3480-
char qn[CBM_SZ_512];
3481-
snprintf(qn, sizeof(qn), "%s.__helm_dep__.%s", l->ctx->project_name, name);
3482-
const cbm_gbuf_node_t *known = cbm_gbuf_find_by_qn(l->ctx->gbuf, qn);
3483-
if (known) {
3484-
return known->id;
3485-
}
3486-
return cbm_gbuf_upsert_node(l->ctx->gbuf, "Chart", name, qn, file, SKIP_ONE, 0,
3487-
"{\"source\":\"helm\",\"external\":true}");
3627+
/* The external Chart of `name` in repository `url` (NULL: unknown), the one
3628+
* chart dependencies use. */
3629+
static int64_t k8s_external_chart(k8s_dep_link_t *l, const char *name, const char *url,
3630+
const char *file) {
3631+
return cbm_pipeline_external_chart(l->ctx, name, url, file);
34883632
}
34893633

34903634
/* A HelmRelease whose chartRef names OCIRepository `oci` (its USAGE edge
34913635
* {"ref":"chartRef"}, resolved with the references) deploys the chart that
3492-
* repository fetches: the shared external Chart of the URL's last segment. */
3636+
* repository fetches: the external Chart of that OCI reference (its last
3637+
* segment the chart, the rest its repository). */
34933638
static void k8s_link_chart_refs(k8s_dep_link_t *l, const k8s_hr_t *oci) {
34943639
const char *url = oci->chart;
34953640
size_t end = strlen(url);
@@ -3513,6 +3658,8 @@ static void k8s_link_chart_refs(k8s_dep_link_t *l, const k8s_hr_t *oci) {
35133658
snprintf(props, sizeof(props),
35143659
"{\"via\":\"helmrelease\",\"chart\":\"%s\",\"external\":true,\"url\":\"%s\"}",
35153660
esc_name, esc_url);
3661+
char repo[CBM_SZ_1K];
3662+
const char *repo_url = cbm_chart_ref_repo(url, repo, sizeof(repo)) ? repo : NULL;
35163663
const cbm_gbuf_edge_t **in = NULL;
35173664
int n = 0;
35183665
if (cbm_gbuf_find_edges_by_target_type(l->ctx->gbuf, oci->node, "USAGE", &in, &n) != 0) {
@@ -3523,7 +3670,8 @@ static void k8s_link_chart_refs(k8s_dep_link_t *l, const k8s_hr_t *oci) {
35233670
const char *p = in[i]->properties_json;
35243671
const cbm_gbuf_node_t *hr = cbm_gbuf_find_by_id(l->ctx->gbuf, in[i]->source_id);
35253672
if (p && strstr(p, "\"ref\":\"chartRef\"") && hr && hr->file_path) {
3526-
k8s_deploys_edge(l, hr->id, k8s_external_chart(l, name, hr->file_path), props);
3673+
k8s_deploys_edge(l, hr->id, k8s_external_chart(l, name, repo_url, hr->file_path),
3674+
props);
35273675
}
35283676
}
35293677
}
@@ -3542,7 +3690,7 @@ static void k8s_prop_add(char *out, size_t out_sz, int *w, const char *key, cons
35423690
* build: DEPLOYS {via: helmcharts, external, chart, url, version, release,
35433691
* namespace, values, line} from the kustomization (its Kustomization
35443692
* Resource, else its File -- the unit a Flux / Argo deploy reaches) to the
3545-
* shared external Chart of that name. */
3693+
* external Chart of that repository and name. */
35463694
static void k8s_link_kcharts(k8s_dep_link_t *l, const k8s_facts_t *f) {
35473695
for (int i = 0; i < f->kchart_n; i++) {
35483696
const k8s_kchart_t *c = &f->kcharts[i];
@@ -3563,7 +3711,7 @@ static void k8s_link_kcharts(k8s_dep_link_t *l, const k8s_facts_t *f) {
35633711
continue;
35643712
}
35653713
snprintf(props + w, sizeof(props) - (size_t)w, ",\"line\":%d}", c->line);
3566-
k8s_deploys_edge(l, src, k8s_external_chart(l, c->chart, c->kust), props);
3714+
k8s_deploys_edge(l, src, k8s_external_chart(l, c->chart, c->url, c->kust), props);
35673715
}
35683716
}
35693717

@@ -4230,7 +4378,9 @@ static void k8s_link_deploys(cbm_pipeline_ctx_t *ctx, k8s_kusts_t *ks, k8s_bundl
42304378
k8s_deploys_edge(&l, hr->node, k8s_chart_node(&l, dir), props);
42314379
}
42324380
} else if (src && src->file_path && !strchr(hr->chart, '/') && !strchr(hr->chart, '$')) {
4233-
k8s_deploys_edge(&l, hr->node, k8s_external_chart(&l, hr->chart, src->file_path),
4381+
/* the source's URL is not read (a HelmRepository's spec.url):
4382+
* the per-project external Chart */
4383+
k8s_deploys_edge(&l, hr->node, k8s_external_chart(&l, hr->chart, NULL, src->file_path),
42344384
props);
42354385
}
42364386
}

‎src/pipeline/pass_operator.c‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1640,7 +1640,7 @@ static CBMHashTable *gc_chart_index(const cbm_gbuf_t *gb) {
16401640
const char *qn = nodes[i]->qualified_name;
16411641
const char *slash = fp ? strrchr(fp, '/') : NULL;
16421642
const char *base = slash ? slash + SKIP_ONE : fp;
1643-
if (!fp || !qn || strstr(qn, ".__helm_dep__.") ||
1643+
if (!fp || !qn || cbm_pipeline_chart_is_external(nodes[i]) ||
16441644
(strcmp(base, "Chart.yaml") != 0 && strcmp(base, "Chart.yml") != 0) || !slash) {
16451645
continue;
16461646
}

0 commit comments

Comments
 (0)