mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Merge pull request #2677 from DeusData/feat/deploy-graph-gaps
feat(deploy): render Helm images, mark default-valued images, link crds and werf charts
This commit is contained in:
19 files changed
+8621
-131
No files matched your search
@@ -537,6 +537,7 @@ PIPELINE_SRCS = \
|
||||
src/pipeline/pass_compile_commands.c \
|
||||
src/pipeline/pass_infrascan.c \
|
||||
src/pipeline/pass_k8s.c \
|
||||
src/pipeline/helm_template.c \
|
||||
src/pipeline/pass_operator.c \
|
||||
src/pipeline/pass_shell.c \
|
||||
src/pipeline/pass_terraform.c \
|
||||
|
||||
@@ -333,6 +333,7 @@ The install script placed beside the binary is **reported, not deleted**: uninst
|
||||
- `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
|
||||
- Helm templates are rendered for their images: with the chart's values (the `Chart` `DEPLOYS` them, `default_of`: the values keys read) and with each release's (a Flux HelmRelease, a kustomize `helmCharts` entry, a Terraform `helm_release`: `override_of`). An image the render cannot know is left out, never guessed: after a change of values it does not run exactly, a `tpl` or include it cannot look into, a release value it cannot read, or a work budget it runs past. Known limit: each template file renders on its own, while Helm renders a chart's files with one shared `.Values` (a subchart's is its parent's `.Values.<alias>`), so a chart that changes `.Values` in one file and reads it in another can still show a wrong image
|
||||
- `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`, ...)
|
||||
- `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`
|
||||
|
||||
+5
-1
@@ -734,7 +734,9 @@ typedef struct {
|
||||
* from the URL `name`; a HelmRelease whose chartRef names it deploys the
|
||||
* chart there.
|
||||
* CBM_K8S_FACT_IMAGE: object def_index runs its container `container`
|
||||
* (NULL: unnamed) from image `name` (a literal reference); via "container".
|
||||
* (NULL: unnamed) from image `name` (a literal reference, or a Helm
|
||||
* template's placeholder `<...>`, which names none); via "container";
|
||||
* `line` the image's line, start_line / end_line the container's.
|
||||
* CBM_K8S_FACT_ENV: object def_index sets environment variable `name` in
|
||||
* its container `container` (NULL: unnamed) at `line`; via = where the
|
||||
* value comes from ("value", or the valueFrom kind) -- never the value.
|
||||
@@ -773,6 +775,8 @@ typedef struct {
|
||||
const char *url; /* DEPLOY (kustomize): the chart repository */
|
||||
const char *release; /* DEPLOY (kustomize): the release name */
|
||||
int line;
|
||||
int start_line; /* IMAGE: the container mapping's first line */
|
||||
int end_line; /* IMAGE: its last line */
|
||||
} CBMK8sFact;
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -1487,12 +1487,18 @@ static void k8s_container(k8s_walk_t *w, const char *path, TSNode container) {
|
||||
const char *cname = k8s_text(w, container, "name");
|
||||
cname = cname && cname[0] && !strstr(cname, "{{") ? cname : NULL;
|
||||
const char *image = k8s_text(w, container, "image");
|
||||
if (k8s_image_literal(image)) {
|
||||
/* a Helm template's image is its skeleton placeholder: pass_k8s.c renders it */
|
||||
if (k8s_image_literal(image) || (image && strchr(image, '<'))) {
|
||||
TSNode iv = k8s_child(w, container, "image");
|
||||
CBMK8sFact f = {.fact = CBM_K8S_FACT_IMAGE,
|
||||
.def_index = w->def_index,
|
||||
.name = image,
|
||||
.container = cname,
|
||||
.via = "container"};
|
||||
.via = "container",
|
||||
.line =
|
||||
ts_node_is_null(iv) ? 0 : (int)ts_node_start_point(iv).row + SKIP_ONE,
|
||||
.start_line = (int)ts_node_start_point(container).row + SKIP_ONE,
|
||||
.end_line = (int)ts_node_end_point(container).row + SKIP_ONE};
|
||||
cbm_k8sfact_push(&w->ctx->result->k8s_facts, w->ctx->arena, f);
|
||||
}
|
||||
k8s_iter_t env = k8s_iter(k8s_child(w, container, "env"));
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* helm_template.h -- Helm values and the rendering of a chart template's
|
||||
* image references (pass_k8s.c). Internal to the pipeline.
|
||||
*
|
||||
* Values are JSON (yyjson mutable values), as Helm holds them after its
|
||||
* YAML -> JSON step: go-yaml v2 scalars (YAML 1.1 booleans), every number a
|
||||
* float64. A template renders with Go text/template semantics and the part
|
||||
* of Sprig image expressions use; anything else -- a function not modelled,
|
||||
* .Release / .Capabilities / lookup, a missing helper, a parse error on the
|
||||
* way -- is UNKNOWN, and an image that reads an UNKNOWN is no image. Nothing
|
||||
* is guessed: a value is what Helm would render, or none.
|
||||
*/
|
||||
#ifndef CBM_HELM_TEMPLATE_H
|
||||
#define CBM_HELM_TEMPLATE_H
|
||||
|
||||
#include "foundation/arena.h"
|
||||
|
||||
#include "tree_sitter/api.h"
|
||||
#include <yyjson/yyjson.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* The first document of YAML text src as a value of doc (Helm's scalar
|
||||
* types); NULL when it does not parse. An empty document is an empty map. */
|
||||
yyjson_mut_val *cbm_helm_values_parse(yyjson_mut_doc *doc, const char *src, size_t len);
|
||||
|
||||
/* The YAML node `node` of text `src` (already parsed, e.g. a HelmRelease's
|
||||
* spec.values) as a value of doc; NULL when it holds an unknown part. */
|
||||
yyjson_mut_val *cbm_helm_values_node(yyjson_mut_doc *doc, const char *src, TSNode node);
|
||||
|
||||
/* Helm's coalesce: a deep copy of `base` with `over` merged in -- maps merge
|
||||
* key by key, any other value replaces, a null deletes. Either may be NULL. */
|
||||
yyjson_mut_val *cbm_helm_values_coalesce(yyjson_mut_doc *doc, yyjson_mut_val *base,
|
||||
yyjson_mut_val *over);
|
||||
|
||||
/* Helm --set NAME=VALUE into map `root`: NAME's `.` separates keys (`\.`
|
||||
* escapes one), `[n]` indexes a list; VALUE is typed like --set (int, bool,
|
||||
* null, else string) unless `as_string`. False when NAME is malformed. */
|
||||
bool cbm_helm_values_set(yyjson_mut_doc *doc, yyjson_mut_val *root, const char *name,
|
||||
const char *value, bool as_string);
|
||||
|
||||
/* A values scalar as a template prints it (a number like Go's %v: 1.10 ->
|
||||
* 1.1) into out; false for a map, a list or null. */
|
||||
bool cbm_helm_scalar_text(yyjson_mut_val *v, char *out, size_t sz);
|
||||
|
||||
/* A chart's metadata, as its templates see it under .Chart -- and where its
|
||||
* templates are: .Template.BasePath as Helm sets it (`<chart path>/templates`,
|
||||
* the chart path its root's name and `/charts/<name or alias>` per subchart
|
||||
* below it; NULL: not known) and the chart's directory as its template files
|
||||
* are added to the defines ("" at the repository root). */
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const char *version;
|
||||
const char *app_version;
|
||||
const char *base_path;
|
||||
const char *dir;
|
||||
} cbm_helm_chart_meta_t;
|
||||
|
||||
/* The named templates (`define` / `block`) of one chart family -- a chart and
|
||||
* the charts below it share one namespace; for one name the define of the
|
||||
* file nearest the family root wins (Helm loads deeper files first), then
|
||||
* the alphabetically first path. */
|
||||
typedef struct cbm_helm_defines cbm_helm_defines_t;
|
||||
|
||||
cbm_helm_defines_t *cbm_helm_defines_new(void);
|
||||
/* Register the defines of template file `rel` (text src, len). The text is
|
||||
* copied; its parse lives as long as the set. */
|
||||
void cbm_helm_defines_add(cbm_helm_defines_t *d, const char *rel, const char *src, size_t len);
|
||||
void cbm_helm_defines_free(cbm_helm_defines_t *d);
|
||||
|
||||
/* A probe: the `image:` key on a template line. Each time rendering passes
|
||||
* that line it captures the scalar written after the key. */
|
||||
typedef struct {
|
||||
int line; /* 1-based line of the key in the template */
|
||||
int n; /* captures */
|
||||
int cap; /* room in image / keys (the render's own) */
|
||||
const char **image; /* the rendered scalar, YAML quoting removed; NULL: UNKNOWN */
|
||||
const char **keys; /* JSON array of the values paths it read; NULL: none */
|
||||
} cbm_helm_probe_t;
|
||||
|
||||
/* Render template `rel` (text src, len) with `values` (its .Values; NULL:
|
||||
* an empty map), `chart` and the family's defines, filling `probes` (strings
|
||||
* in arena a, which gets only these: the render works in an arena of its own).
|
||||
* Values paths are dot-joined keys, a key's own `.` escaped (`\.`), as --set
|
||||
* writes them. False when the render ran past its step, output or memory
|
||||
* budget -- a template Helm would not finish either (logged): then every
|
||||
* capture is UNKNOWN. */
|
||||
bool cbm_helm_render(CBMArena *a, const char *rel, const char *src, size_t len,
|
||||
yyjson_mut_val *values, const cbm_helm_chart_meta_t *chart,
|
||||
const cbm_helm_defines_t *defs, cbm_helm_probe_t *probes, int nprobes);
|
||||
|
||||
#endif /* CBM_HELM_TEMPLATE_H */
|
||||
+91
-4
@@ -25,6 +25,7 @@
|
||||
*/
|
||||
#include "foundation/constants.h"
|
||||
#include "foundation/compat.h"
|
||||
#include "foundation/str_util.h" /* cbm_json_escape */
|
||||
#include "pipeline/pipeline_internal.h"
|
||||
#include "cbm.h"
|
||||
#include "helpers.h" /* cbm_env_address */
|
||||
@@ -783,9 +784,33 @@ static const char *ci_env_lookup(const ci_env_t *env, const char *name, size_t n
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* JSON array `list` (NULL: empty) with identifier `name` appended once. */
|
||||
static const char *ci_names_add(CBMArena *a, const char *list, const char *name, size_t nl) {
|
||||
if (!list) {
|
||||
return cbm_arena_sprintf(a, "[\"%.*s\"]", (int)nl, name);
|
||||
}
|
||||
char quoted[CBM_SZ_256];
|
||||
snprintf(quoted, sizeof(quoted), "\"%.*s\"", (int)nl, name);
|
||||
if (strstr(list, quoted)) {
|
||||
return list;
|
||||
}
|
||||
return cbm_arena_sprintf(a, "%.*s,%s]", (int)(strlen(list) - SKIP_ONE), list, quoted);
|
||||
}
|
||||
|
||||
static const char *ci_interp_marks(CBMArena *a, const char *s, const ci_env_t *env,
|
||||
const char **dflt_of, const char **ovr_of);
|
||||
|
||||
/* `s` with ${X} / ${X:-d} / ${X-d} / $X expanded from env; NULL when a
|
||||
* variable has no value (and no default). */
|
||||
static const char *ci_interp(CBMArena *a, const char *s, const ci_env_t *env) {
|
||||
return ci_interp_marks(a, s, env, NULL, NULL);
|
||||
}
|
||||
|
||||
/* ci_interp; `dflt_of` (may be NULL) gains, as a JSON array, the variables
|
||||
* whose inline default was used, `ovr_of` those with an inline default that
|
||||
* env set instead. */
|
||||
static const char *ci_interp_marks(CBMArena *a, const char *s, const ci_env_t *env,
|
||||
const char **dflt_of, const char **ovr_of) {
|
||||
if (!s || !strchr(s, '$')) {
|
||||
return s;
|
||||
}
|
||||
@@ -839,6 +864,11 @@ static const char *ci_interp(CBMArena *a, const char *s, const ci_env_t *env) {
|
||||
}
|
||||
use = dflt;
|
||||
ul = dl;
|
||||
if (dflt_of) {
|
||||
*dflt_of = ci_names_add(a, *dflt_of, name, nl);
|
||||
}
|
||||
} else if (dflt && ovr_of) {
|
||||
*ovr_of = ci_names_add(a, *ovr_of, name, nl);
|
||||
}
|
||||
if (len + ul + SKIP_ONE > cap) {
|
||||
return NULL;
|
||||
@@ -1054,7 +1084,53 @@ static int ci_compose_env(const ci_t *c, TSNode svc, const ci_env_t *dotenv, con
|
||||
return k;
|
||||
}
|
||||
|
||||
/* A compose build's `args` (a mapping, or a list of "K=V" items), their
|
||||
* values interpolated, as a JSON object; NULL: none. An argument without a
|
||||
* value (the environment's), with an unknown one or with a secret (by name or
|
||||
* value -- never kept in the graph, never an image) is left out. */
|
||||
static const char *ci_compose_build_args(const ci_t *c, TSNode build, const ci_env_t *env) {
|
||||
TSNode args = ts_node_is_null(build) ? build : ci_get(c, build, "args");
|
||||
if (ts_node_is_null(args)) {
|
||||
return NULL;
|
||||
}
|
||||
int n = 0;
|
||||
TSNode *items = ci_items(c, args, &n);
|
||||
ci_pair_t *pairs = items ? NULL : ci_pairs(c, args, &n);
|
||||
const char *json = NULL;
|
||||
for (int i = 0; i < n; i++) {
|
||||
const char *key = NULL;
|
||||
const char *raw = NULL;
|
||||
if (items) {
|
||||
const char *s = ci_scalar(c, items[i], NULL);
|
||||
const char *eq = s ? strchr(s, '=') : NULL;
|
||||
key = eq ? cbm_arena_strndup(c->a, s, (size_t)(eq - s)) : NULL;
|
||||
raw = eq ? eq + SKIP_ONE : NULL;
|
||||
} else {
|
||||
key = pairs[i].key;
|
||||
raw = ts_node_is_null(pairs[i].value) ? NULL : ci_scalar(c, pairs[i].value, NULL);
|
||||
}
|
||||
const char *value = raw ? ci_interp(c->a, raw, env) : NULL;
|
||||
if (!key || !key[0] || !value || cbm_is_secret_binding(key, value)) {
|
||||
continue;
|
||||
}
|
||||
/* JSON escaping writes at most 6 bytes a byte: nothing is cut */
|
||||
int kn = (int)(strlen(key) * 6 + SKIP_ONE);
|
||||
int vn = (int)(strlen(value) * 6 + SKIP_ONE);
|
||||
char *ek = cbm_arena_alloc(c->a, (size_t)kn);
|
||||
char *ev = cbm_arena_alloc(c->a, (size_t)vn);
|
||||
if (!ek || !ev) {
|
||||
return NULL;
|
||||
}
|
||||
cbm_json_escape(ek, kn, key);
|
||||
cbm_json_escape(ev, vn, value);
|
||||
json = cbm_arena_sprintf(c->a, "%s%s\"%s\":\"%s\"", json ? json : "{", json ? "," : "", ek,
|
||||
ev);
|
||||
}
|
||||
return json ? cbm_arena_sprintf(c->a, "%s}", json) : NULL;
|
||||
}
|
||||
|
||||
static void ci_compose(const ci_t *c, TSNode root, const char *dir, const ci_env_t *env) {
|
||||
static const ci_env_t no_env = {0};
|
||||
int ns = 0;
|
||||
ci_pair_t *svcs = ci_pairs(c, ci_get(c, root, "services"), &ns);
|
||||
for (int i = 0; i < ns; i++) {
|
||||
@@ -1064,16 +1140,27 @@ static void ci_compose(const ci_t *c, TSNode root, const char *dir, const ci_env
|
||||
}
|
||||
int row = 0;
|
||||
const char *raw = ci_scalar(c, ci_get(c, svc, "image"), &row);
|
||||
const char *image = raw ? ci_interp(c->a, raw, env) : NULL;
|
||||
const char *df = ci_compose_dockerfile(c, ci_get(c, svc, "build"), dir, env);
|
||||
const char *dflt_of = NULL;
|
||||
const char *ovr_of = NULL;
|
||||
const char *image = raw ? ci_interp_marks(c->a, raw, env, &dflt_of, &ovr_of) : NULL;
|
||||
/* With .env replacing a default: what the defaults alone would name. */
|
||||
const char *default_image = image && ovr_of ? ci_interp(c->a, raw, &no_env) : NULL;
|
||||
TSNode build = ci_get(c, svc, "build");
|
||||
const char *df = ci_compose_dockerfile(c, build, dir, env);
|
||||
if (c->site_fn && image && df) {
|
||||
cbm_build_site_t site = {
|
||||
.via = "compose", .image = image, .dockerfile = df, .line = row + SKIP_ONE};
|
||||
cbm_build_site_t site = {.via = "compose",
|
||||
.image = image,
|
||||
.dockerfile = df,
|
||||
.build_args = ci_compose_build_args(c, build, env),
|
||||
.line = row + SKIP_ONE};
|
||||
c->site_fn(&site, c->ud);
|
||||
}
|
||||
if (c->service_fn) {
|
||||
cbm_compose_service_t s = {.name = svcs[i].key,
|
||||
.image = image,
|
||||
.image_default_of = image ? dflt_of : NULL,
|
||||
.image_override_of = image ? ovr_of : NULL,
|
||||
.default_image = default_image,
|
||||
.dockerfile = df,
|
||||
.line = (int)ts_node_start_point(svc).row,
|
||||
.end_line = (int)ts_node_end_point(svc).row + SKIP_ONE};
|
||||
|
||||
+610
-49
@@ -108,12 +108,31 @@ typedef struct {
|
||||
CBMHashTable *dirs; /* every directory holding a file (not the root) */
|
||||
} dk_index_t;
|
||||
|
||||
/* Where a value came from: the ARGs whose default (`dflt`) or build argument
|
||||
* (`ovr`) supplied part of it, as '\x1f'-separated names; NULL: none. */
|
||||
typedef struct {
|
||||
const char *dflt;
|
||||
const char *ovr;
|
||||
} dk_prov_t;
|
||||
|
||||
typedef struct dk_var {
|
||||
const char *name;
|
||||
const char *value; /* NULL: declared without a value */
|
||||
bool arg; /* an ARG: a build argument can set it */
|
||||
dk_prov_t prov;
|
||||
struct dk_var *next;
|
||||
} dk_var_t;
|
||||
|
||||
/* An image reference of FROM / COPY --from, linked once the file is read. */
|
||||
typedef struct {
|
||||
int64_t img;
|
||||
const char *ref;
|
||||
const char *via;
|
||||
int stage;
|
||||
int line;
|
||||
dk_prov_t prov;
|
||||
} dk_ref_t;
|
||||
|
||||
typedef struct {
|
||||
cbm_pipeline_ctx_t *ctx;
|
||||
CBMArena *a;
|
||||
@@ -131,6 +150,14 @@ typedef struct {
|
||||
dk_entry_t entry; /* the current stage's ENTRYPOINT */
|
||||
dk_entry_t cmd; /* the current stage's CMD */
|
||||
const dk_index_t *ix;
|
||||
dk_ref_t *refs; /* FROM / COPY --from references, in file order */
|
||||
int nrefs;
|
||||
int refs_cap;
|
||||
/* A build's view (dk_build_args): its build arguments set the ARGs they
|
||||
* name, and the references they change link from the Image it builds. */
|
||||
const dk_var_t *build_args;
|
||||
int64_t built; /* 0: the Dockerfile's own view */
|
||||
const char *build_props; /* the build's tool / file / build_line members */
|
||||
} dk_t;
|
||||
|
||||
static bool dk_type(TSNode n, const char *t) {
|
||||
@@ -172,14 +199,72 @@ static const dk_var_t *dk_find(const dk_var_t *v, const char *name, size_t nl) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void dk_set(dk_t *d, dk_var_t **list, const char *name, const char *value) {
|
||||
static void dk_set(dk_t *d, dk_var_t **list, const char *name, const char *value, bool arg,
|
||||
dk_prov_t prov) {
|
||||
dk_var_t *v = cbm_arena_alloc(d->a, sizeof(*v));
|
||||
if (v) {
|
||||
*v = (dk_var_t){.name = name, .value = value, .next = *list};
|
||||
*v = (dk_var_t){.name = name, .value = value, .arg = arg, .prov = prov, .next = *list};
|
||||
*list = v;
|
||||
}
|
||||
}
|
||||
|
||||
enum { DK_NAME_SEP = '\x1f' };
|
||||
|
||||
static bool dk_names_has(const char *list, const char *name, size_t nl) {
|
||||
for (const char *p = list; p && *p;) {
|
||||
size_t l = strcspn(p, "\x1f");
|
||||
if (l == nl && strncmp(p, name, nl) == 0) {
|
||||
return true;
|
||||
}
|
||||
p += l;
|
||||
p += *p ? SKIP_ONE : 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* `list` with `name` appended unless it holds it already. */
|
||||
static const char *dk_names_add(dk_t *d, const char *list, const char *name, size_t nl) {
|
||||
if (dk_names_has(list, name, nl)) {
|
||||
return list;
|
||||
}
|
||||
return list && list[0] ? cbm_arena_sprintf(d->a, "%s%c%.*s", list, DK_NAME_SEP, (int)nl, name)
|
||||
: cbm_arena_strndup(d->a, name, nl);
|
||||
}
|
||||
|
||||
static const char *dk_names_union(dk_t *d, const char *a, const char *b) {
|
||||
for (const char *p = b; p && *p;) {
|
||||
size_t l = strcspn(p, "\x1f");
|
||||
a = dk_names_add(d, a, p, l);
|
||||
p += l;
|
||||
p += *p ? SKIP_ONE : 0;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
static void dk_prov_merge(dk_t *d, dk_prov_t *into, dk_prov_t from) {
|
||||
into->dflt = dk_names_union(d, into->dflt, from.dflt);
|
||||
into->ovr = dk_names_union(d, into->ovr, from.ovr);
|
||||
}
|
||||
|
||||
/* `,"key":["A","B"]` for a non-empty name list, else "". */
|
||||
static const char *dk_names_member(dk_t *d, const char *key, const char *list) {
|
||||
if (!list || !list[0]) {
|
||||
return "";
|
||||
}
|
||||
const char *out = cbm_arena_sprintf(d->a, ",\"%s\":[", key);
|
||||
for (const char *p = list; out && *p;) {
|
||||
size_t l = strcspn(p, "\x1f");
|
||||
char esc[CBM_SZ_256];
|
||||
char name[CBM_SZ_256];
|
||||
snprintf(name, sizeof(name), "%.*s", (int)l, p);
|
||||
cbm_json_escape(esc, (int)sizeof(esc), name);
|
||||
out = cbm_arena_sprintf(d->a, "%s%s\"%s\"", out, p == list ? "" : ",", esc);
|
||||
p += l;
|
||||
p += *p ? SKIP_ONE : 0;
|
||||
}
|
||||
return out ? cbm_arena_sprintf(d->a, "%s]", out) : "";
|
||||
}
|
||||
|
||||
/* ASCII case-insensitive equality (stage names, `scratch`). */
|
||||
static bool dk_ieq(const char *a, const char *b) {
|
||||
for (; *a && *b; a++, b++) {
|
||||
@@ -195,8 +280,10 @@ static bool dk_name_char(char c) {
|
||||
}
|
||||
|
||||
/* `text` with $X / ${X} / ${X:-d} / ${X-d} expanded from `vars`; NULL when a
|
||||
* reference has no value. */
|
||||
static const char *dk_expand(dk_t *d, const dk_var_t *vars, const char *text) {
|
||||
* reference has no value. `prov` (may be NULL) gains the ARGs whose default
|
||||
* or build argument supplied a value -- an inline `${X:-d}` default counts
|
||||
* only for a declared ARG X, the one thing a build argument can set. */
|
||||
static const char *dk_expand(dk_t *d, const dk_var_t *vars, const char *text, dk_prov_t *prov) {
|
||||
size_t cap = strlen(text) + SKIP_ONE;
|
||||
size_t len = 0;
|
||||
char *out = cbm_arena_alloc(d->a, cap);
|
||||
@@ -249,6 +336,11 @@ static const char *dk_expand(dk_t *d, const dk_var_t *vars, const char *text) {
|
||||
return NULL;
|
||||
}
|
||||
const char *from = val ? val : dflt;
|
||||
if (prov && v && val) {
|
||||
dk_prov_merge(d, prov, v->prov);
|
||||
} else if (prov && v && v->arg) {
|
||||
prov->dflt = dk_names_add(d, prov->dflt, name, nl);
|
||||
}
|
||||
if (len + vl + SKIP_ONE > cap) {
|
||||
size_t ncap = (len + vl + SKIP_ONE) * PAIR_LEN;
|
||||
char *grown = cbm_arena_alloc(d->a, ncap);
|
||||
@@ -370,6 +462,139 @@ static int dk_stage_of(const dk_t *d, const char *ref) {
|
||||
return found;
|
||||
}
|
||||
|
||||
/* A path component: [a-z0-9]+ joined by `.`, `_`, `__` or `-`+. */
|
||||
static bool dk_ref_component(const char *s, size_t n) {
|
||||
if (n == 0 || !isalnum((unsigned char)s[0]) || !isalnum((unsigned char)s[n - SKIP_ONE])) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
unsigned char c = (unsigned char)s[i];
|
||||
if (isupper(c)) {
|
||||
return false;
|
||||
}
|
||||
if (isalnum(c)) {
|
||||
continue;
|
||||
}
|
||||
size_t run = SKIP_ONE;
|
||||
while (i + run < n && s[i + run] == c) {
|
||||
run++;
|
||||
}
|
||||
if (!((c == '.' && run == SKIP_ONE) || (c == '_' && run <= PAIR_LEN) || c == '-')) {
|
||||
return false;
|
||||
}
|
||||
i += run - SKIP_ONE;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* A registry host: DNS labels (`.`-joined), or an IPv6 literal, with an
|
||||
* optional :port. */
|
||||
static bool dk_ref_domain(const char *s, size_t n) {
|
||||
const char *colon = memchr(s, ':', n);
|
||||
if (n && s[0] == '[') {
|
||||
const char *close = memchr(s, ']', n);
|
||||
colon = close && (size_t)(close - s) + SKIP_ONE < n ? close + SKIP_ONE : NULL;
|
||||
if (!close) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
size_t hl = colon ? (size_t)(colon - s) : n;
|
||||
if (hl == 0) {
|
||||
return false;
|
||||
}
|
||||
if (s[0] != '[') {
|
||||
for (size_t i = 0; i < hl; i++) {
|
||||
unsigned char c = (unsigned char)s[i];
|
||||
if (!isalnum(c) && c != '.' && c != '-') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (size_t i = hl + SKIP_ONE; colon && i < n; i++) {
|
||||
if (!isdigit((unsigned char)s[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !colon || hl + SKIP_ONE < n;
|
||||
}
|
||||
|
||||
bool cbm_image_ref_valid(const char *ref) {
|
||||
if (!ref || !ref[0]) {
|
||||
return false;
|
||||
}
|
||||
size_t n = strlen(ref);
|
||||
const char *at = strchr(ref, '@');
|
||||
if (at) {
|
||||
const char *colon = strchr(at, ':');
|
||||
if (!colon || colon == at + SKIP_ONE || strlen(colon + SKIP_ONE) < CBM_SZ_32) {
|
||||
return false;
|
||||
}
|
||||
for (const char *p = at + SKIP_ONE; p < colon; p++) {
|
||||
if (!isalnum((unsigned char)*p) && !strchr("+.-_", *p)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const char *p = colon + SKIP_ONE; *p; p++) {
|
||||
if (!isxdigit((unsigned char)*p)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
n = (size_t)(at - ref);
|
||||
}
|
||||
/* the tag: after the last ':' that follows the last '/' */
|
||||
size_t name_end = n;
|
||||
for (size_t i = n; i > 0; i--) {
|
||||
if (ref[i - SKIP_ONE] == '/') {
|
||||
break;
|
||||
}
|
||||
if (ref[i - SKIP_ONE] == ':') {
|
||||
name_end = i - SKIP_ONE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (name_end < n) {
|
||||
size_t tl = n - name_end - SKIP_ONE;
|
||||
const char *tag = ref + name_end + SKIP_ONE;
|
||||
if (tl == 0 || tl > CBM_SZ_128 || !(isalnum((unsigned char)tag[0]) || tag[0] == '_')) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < tl; i++) {
|
||||
if (!isalnum((unsigned char)tag[i]) && !strchr("_.-", tag[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
const char *p = ref;
|
||||
const char *end = ref + name_end;
|
||||
const char *slash = memchr(p, '/', (size_t)(end - p));
|
||||
if (slash) {
|
||||
size_t fl = (size_t)(slash - p);
|
||||
bool host = memchr(p, '.', fl) || memchr(p, ':', fl) ||
|
||||
(fl == strlen("localhost") && memcmp(p, "localhost", fl) == 0);
|
||||
if (host) {
|
||||
if (!dk_ref_domain(p, fl)) {
|
||||
return false;
|
||||
}
|
||||
p = slash + SKIP_ONE;
|
||||
}
|
||||
}
|
||||
if (p >= end) {
|
||||
return false;
|
||||
}
|
||||
while (p < end) {
|
||||
const char *s = memchr(p, '/', (size_t)(end - p));
|
||||
size_t cl = s ? (size_t)(s - p) : (size_t)(end - p);
|
||||
if (!dk_ref_component(p, cl)) {
|
||||
return false;
|
||||
}
|
||||
p += cl + (s ? SKIP_ONE : 0);
|
||||
if (s && p >= end) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int64_t cbm_pipeline_image_node(cbm_pipeline_ctx_t *ctx, const char *ref) {
|
||||
char repo[CBM_SZ_512];
|
||||
char reg[CBM_SZ_512];
|
||||
@@ -438,19 +663,101 @@ int64_t cbm_pipeline_env_setter(cbm_pipeline_ctx_t *ctx, const cbm_gbuf_node_t *
|
||||
return id;
|
||||
}
|
||||
|
||||
/* DEPENDS_ON from the Dockerfile to the Image of resolved reference `ref`. */
|
||||
static void dk_link(dk_t *d, const char *ref, const char *via, int line) {
|
||||
/* Record the Image of resolved reference `ref`, linked by dk_refs_link. */
|
||||
static void dk_link(dk_t *d, const char *ref, const char *via, int line, dk_prov_t prov) {
|
||||
int64_t img = cbm_pipeline_image_node(d->ctx, ref);
|
||||
if (img <= 0) {
|
||||
return;
|
||||
}
|
||||
char esc_ref[CBM_SZ_1K];
|
||||
cbm_json_escape(esc_ref, (int)sizeof(esc_ref), ref);
|
||||
char props[CBM_SZ_2K];
|
||||
snprintf(props, sizeof(props), "{\"via\":\"%s\",\"image\":\"%s\",\"stage\":%d,\"line\":%d}",
|
||||
via, esc_ref, d->stage, line);
|
||||
cbm_gbuf_insert_edge(d->ctx->gbuf, d->module, img, "DEPENDS_ON", props);
|
||||
d->edges++;
|
||||
if (d->nrefs == d->refs_cap) {
|
||||
int cap = d->refs_cap ? d->refs_cap * PAIR_LEN : DK_MAX_STAGES;
|
||||
dk_ref_t *grown = cbm_arena_alloc(d->a, (size_t)cap * sizeof(*grown));
|
||||
if (!grown) {
|
||||
return;
|
||||
}
|
||||
if (d->nrefs) {
|
||||
memcpy(grown, d->refs, (size_t)d->nrefs * sizeof(*grown));
|
||||
}
|
||||
d->refs = grown;
|
||||
d->refs_cap = cap;
|
||||
}
|
||||
d->refs[d->nrefs++] = (dk_ref_t){img, ref, via, d->stage, line, prov};
|
||||
}
|
||||
|
||||
/* The names a default supplies to the references of Image `img`: their union,
|
||||
* or NULL when any of them names it without a default (a literal reference
|
||||
* wins -- the edges to one Image are one edge, so every one of them carries
|
||||
* the same mark). */
|
||||
static const char *dk_refs_defaults(dk_t *d, int64_t img) {
|
||||
const char *names = NULL;
|
||||
for (int i = 0; i < d->nrefs; i++) {
|
||||
if (d->refs[i].img != img) {
|
||||
continue;
|
||||
}
|
||||
if (!d->refs[i].prov.dflt) {
|
||||
return NULL;
|
||||
}
|
||||
names = dk_names_union(d, names, d->refs[i].prov.dflt);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/* The Dockerfile's own view: DEPENDS_ON from its Module to each Image FROM /
|
||||
* COPY --from names {via, image, stage, line[, default_of: the ARGs whose
|
||||
* defaults named it]}. */
|
||||
static void dk_refs_link(dk_t *d) {
|
||||
for (int i = 0; i < d->nrefs; i++) {
|
||||
const dk_ref_t *r = &d->refs[i];
|
||||
char esc_ref[CBM_SZ_1K];
|
||||
cbm_json_escape(esc_ref, (int)sizeof(esc_ref), r->ref);
|
||||
const char *dflt = dk_names_member(d, "default_of", dk_refs_defaults(d, r->img));
|
||||
const char *props =
|
||||
cbm_arena_sprintf(d->a, "{\"via\":\"%s\",\"image\":\"%s\",\"stage\":%d,\"line\":%d%s}",
|
||||
r->via, esc_ref, r->stage, r->line, dflt);
|
||||
if (props) {
|
||||
cbm_gbuf_insert_edge(d->ctx->gbuf, d->module, r->img, "DEPENDS_ON", props);
|
||||
d->edges++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* A build's view: each Image a reference the build's arguments changed names
|
||||
* -- the first reference to it, its build arguments unioned --
|
||||
* DEPENDS_ON from the Image the build makes {via, image, override_of[,
|
||||
* default_of], dockerfile, stage, line, tool, file, build_line}. A reference
|
||||
* no build argument reached is the Dockerfile's own edge already. */
|
||||
static void dk_refs_link_build(dk_t *d) {
|
||||
for (int i = 0; i < d->nrefs; i++) {
|
||||
const dk_ref_t *r = &d->refs[i];
|
||||
bool first = r->prov.ovr != NULL && r->img != d->built;
|
||||
for (int j = 0; first && j < i; j++) {
|
||||
first = !(d->refs[j].img == r->img && d->refs[j].prov.ovr);
|
||||
}
|
||||
if (!first) {
|
||||
continue;
|
||||
}
|
||||
dk_prov_t all = {0};
|
||||
for (int j = i; j < d->nrefs; j++) {
|
||||
if (d->refs[j].img == r->img && d->refs[j].prov.ovr) {
|
||||
dk_prov_merge(d, &all, d->refs[j].prov);
|
||||
}
|
||||
}
|
||||
char esc_ref[CBM_SZ_1K];
|
||||
char esc_file[CBM_SZ_1K];
|
||||
cbm_json_escape(esc_ref, (int)sizeof(esc_ref), r->ref);
|
||||
cbm_json_escape(esc_file, (int)sizeof(esc_file), d->file);
|
||||
const char *props = cbm_arena_sprintf(
|
||||
d->a,
|
||||
"{\"via\":\"%s\",\"image\":\"%s\"%s%s,\"dockerfile\":\"%s\",\"stage\":%d,"
|
||||
"\"line\":%d,%s}",
|
||||
r->via, esc_ref, dk_names_member(d, "override_of", all.ovr),
|
||||
dk_names_member(d, "default_of", all.dflt), esc_file, r->stage, r->line,
|
||||
d->build_props);
|
||||
if (props) {
|
||||
cbm_gbuf_insert_edge(d->ctx->gbuf, d->built, r->img, "DEPENDS_ON", props);
|
||||
d->edges++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int dk_line(TSNode n) {
|
||||
@@ -467,19 +774,33 @@ static void dk_arg(dk_t *d, TSNode ins) {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
if (d->stage < 0) {
|
||||
dk_set(d, &d->global, name, ts_node_is_null(df) ? NULL : dk_value(d, df));
|
||||
size_t nl = strlen(name);
|
||||
dk_var_t **list = d->stage < 0 ? &d->global : &d->local;
|
||||
/* A build argument sets the ARG it names, in every stage declaring it,
|
||||
* whatever its default. */
|
||||
const dk_var_t *set = dk_find(d->build_args, name, nl);
|
||||
if (set) {
|
||||
dk_set(d, list, name, set->value, true,
|
||||
(dk_prov_t){.ovr = dk_names_add(d, NULL, name, nl)});
|
||||
return;
|
||||
}
|
||||
const char *value = NULL;
|
||||
dk_prov_t prov = {0};
|
||||
if (!ts_node_is_null(df)) {
|
||||
/* A default may name ARGs declared before it (ARG BASE=golang:${GO}). */
|
||||
const char *raw = dk_value(d, df);
|
||||
value = raw ? dk_expand(d, d->local, raw) : NULL;
|
||||
} else {
|
||||
const dk_var_t *g = dk_find(d->global, name, strlen(name));
|
||||
value = g ? g->value : NULL;
|
||||
value = raw ? dk_expand(d, *list, raw, &prov) : NULL;
|
||||
if (value) {
|
||||
prov.dflt = dk_names_add(d, prov.dflt, name, nl);
|
||||
}
|
||||
} else if (d->stage >= 0) {
|
||||
const dk_var_t *g = dk_find(d->global, name, nl);
|
||||
if (g) {
|
||||
value = g->value;
|
||||
prov = g->prov;
|
||||
}
|
||||
}
|
||||
dk_set(d, &d->local, name, value);
|
||||
dk_set(d, list, name, value, true, prov);
|
||||
}
|
||||
|
||||
static void dk_env(dk_t *d, TSNode ins) {
|
||||
@@ -498,7 +819,9 @@ static void dk_env(dk_t *d, TSNode ins) {
|
||||
}
|
||||
const char *raw = ts_node_is_null(vl) ? NULL : dk_value(d, vl);
|
||||
const char *name = dk_text(d, nm);
|
||||
dk_set(d, &d->local, name, raw ? dk_expand(d, d->local, raw) : NULL);
|
||||
dk_prov_t prov = {0};
|
||||
const char *value = raw ? dk_expand(d, d->local, raw, &prov) : NULL;
|
||||
dk_set(d, &d->local, name, value, false, prov);
|
||||
dk_envkey_t *k = d->stage < DK_MAX_STAGES ? cbm_arena_alloc(d->a, sizeof(*k)) : NULL;
|
||||
if (k && name) {
|
||||
*k = (dk_envkey_t){name, dk_line(pair), d->st[d->stage].envs};
|
||||
@@ -515,7 +838,9 @@ static void dk_from(dk_t *d, TSNode ins) {
|
||||
spec = ts_node_named_child(ins, i);
|
||||
}
|
||||
}
|
||||
const char *ref = ts_node_is_null(spec) ? NULL : dk_expand(d, d->global, dk_text(d, spec));
|
||||
dk_prov_t prov = {0};
|
||||
const char *ref =
|
||||
ts_node_is_null(spec) ? NULL : dk_expand(d, d->global, dk_text(d, spec), &prov);
|
||||
d->stage++;
|
||||
d->local = NULL;
|
||||
d->workdir = "/";
|
||||
@@ -523,7 +848,7 @@ static void dk_from(dk_t *d, TSNode ins) {
|
||||
d->cmd.set = false;
|
||||
bool stage = ref && dk_is_stage(d, ref);
|
||||
if (ref && !stage) {
|
||||
dk_link(d, ref, "from", dk_line(ins));
|
||||
dk_link(d, ref, "from", dk_line(ins), prov);
|
||||
}
|
||||
if (d->stage < DK_MAX_STAGES) {
|
||||
d->st[d->stage].parent = stage ? dk_stage_of(d, ref) : -1;
|
||||
@@ -605,7 +930,7 @@ static int dk_copy_json(dk_t *d, const char *p, const char **out, int max) {
|
||||
if (yyjson_is_arr(arr)) {
|
||||
for (yyjson_arr_iter_init(arr, &it); n < max && (v = yyjson_arr_iter_next(&it));) {
|
||||
const char *s = yyjson_get_str(v);
|
||||
out[n++] = s ? dk_expand(d, d->local, cbm_arena_strdup(d->a, s)) : NULL;
|
||||
out[n++] = s ? dk_expand(d, d->local, cbm_arena_strdup(d->a, s), NULL) : NULL;
|
||||
}
|
||||
}
|
||||
if (doc) {
|
||||
@@ -639,11 +964,12 @@ static void dk_copy(dk_t *d, TSNode ins) {
|
||||
if (l > sizeof(FROM) - SKIP_ONE && strncmp(p, FROM, sizeof(FROM) - SKIP_ONE) == 0) {
|
||||
const char *v =
|
||||
cbm_arena_strndup(d->a, p + sizeof(FROM) - SKIP_ONE, l - (sizeof(FROM) - SKIP_ONE));
|
||||
const char *ref = v ? dk_expand(d, d->local, v) : NULL;
|
||||
dk_prov_t prov = {0};
|
||||
const char *ref = v ? dk_expand(d, d->local, v, &prov) : NULL;
|
||||
c->has_from = true;
|
||||
c->from = ref;
|
||||
if (ref && ref[0] && !dk_is_stage(d, ref)) {
|
||||
dk_link(d, ref, "copy-from", dk_line(ins));
|
||||
dk_link(d, ref, "copy-from", dk_line(ins), prov);
|
||||
}
|
||||
}
|
||||
p += l;
|
||||
@@ -652,7 +978,7 @@ static void dk_copy(dk_t *d, TSNode ins) {
|
||||
while (!json && *p && nw < DK_MAX_WORDS) {
|
||||
size_t l = strcspn(p, " \t");
|
||||
const char *w = cbm_arena_strndup(d->a, p, l);
|
||||
words[nw++] = w && strncmp(w, "<<", PAIR_LEN) != 0 ? dk_expand(d, d->local, w) : NULL;
|
||||
words[nw++] = w && strncmp(w, "<<", PAIR_LEN) != 0 ? dk_expand(d, d->local, w, NULL) : NULL;
|
||||
p += l;
|
||||
p += strspn(p, " \t");
|
||||
}
|
||||
@@ -1023,8 +1349,8 @@ static void dk_go_builds(dk_t *d, const char **w, int n) {
|
||||
if (!b) {
|
||||
return;
|
||||
}
|
||||
*b = (dk_build_t){.out = out ? dk_expand(d, d->local, out) : NULL,
|
||||
.pkg = dk_expand(d, d->local, pkg ? pkg : "."),
|
||||
*b = (dk_build_t){.out = out ? dk_expand(d, d->local, out, NULL) : NULL,
|
||||
.pkg = dk_expand(d, d->local, pkg ? pkg : ".", NULL),
|
||||
.workdir = d->workdir,
|
||||
.next = d->st[d->stage].builds};
|
||||
d->st[d->stage].builds = b;
|
||||
@@ -1066,7 +1392,7 @@ static void dk_workdir(dk_t *d, TSNode ins) {
|
||||
continue;
|
||||
}
|
||||
const char *raw = dk_text(d, pn);
|
||||
const char *v = raw ? dk_expand(d, d->local, raw) : NULL;
|
||||
const char *v = raw ? dk_expand(d, d->local, raw, NULL) : NULL;
|
||||
const char *w = v ? v : raw;
|
||||
if (w) {
|
||||
d->workdir = dk_absolute(d, w, d->workdir);
|
||||
@@ -1431,11 +1757,31 @@ static void dk_file(dk_t *d, TSNode root) {
|
||||
dk_entry(d, ins, &d->cmd);
|
||||
}
|
||||
}
|
||||
dk_refs_link(d);
|
||||
dk_spawn(d);
|
||||
dk_envs(d);
|
||||
dk_image_set(d);
|
||||
}
|
||||
|
||||
/* A build's view of the Dockerfile: only what decides its image references
|
||||
* (ARG, ENV, FROM, COPY --from) is read, under the build's arguments. */
|
||||
static void dk_file_build(dk_t *d, TSNode root) {
|
||||
for (uint32_t i = 0; i < ts_node_named_child_count(root); i++) {
|
||||
TSNode ins = ts_node_named_child(root, i);
|
||||
if (dk_type(ins, "arg_instruction")) {
|
||||
dk_arg(d, ins);
|
||||
} else if (dk_type(ins, "env_instruction")) {
|
||||
dk_env(d, ins);
|
||||
} else if (dk_type(ins, "from_instruction")) {
|
||||
dk_from(d, ins);
|
||||
} else if ((dk_type(ins, "copy_instruction") || dk_type(ins, "add_instruction")) &&
|
||||
d->stage >= 0) {
|
||||
dk_copy(d, ins);
|
||||
}
|
||||
}
|
||||
dk_refs_link_build(d);
|
||||
}
|
||||
|
||||
static char *dk_read(const char *path, size_t *len) {
|
||||
FILE *f = cbm_fopen(path, "rb");
|
||||
if (!f) {
|
||||
@@ -1545,12 +1891,15 @@ static void dk_build_site(const cbm_build_site_t *s, void *ud) {
|
||||
char esc_img[CBM_SZ_1K];
|
||||
cbm_json_escape(esc_file, (int)sizeof(esc_file), b->file);
|
||||
cbm_json_escape(esc_img, (int)sizeof(esc_img), s->image);
|
||||
char props[CBM_SZ_4K];
|
||||
snprintf(props, sizeof(props),
|
||||
"{\"via\":\"build\",\"tool\":\"%s\",\"file\":\"%s\",\"image\":\"%s\",\"line\":%d}",
|
||||
s->via, esc_file, esc_img, s->line);
|
||||
cbm_gbuf_insert_edge(b->ctx->gbuf, img, mod->id, "DEPENDS_ON", props);
|
||||
b->edges++;
|
||||
const char *props = cbm_arena_sprintf(
|
||||
b->a,
|
||||
"{\"via\":\"build\",\"tool\":\"%s\",\"file\":\"%s\",\"image\":\"%s\",\"line\":%d%s%s}",
|
||||
s->via, esc_file, esc_img, s->line, s->build_args ? ",\"build_args\":" : "",
|
||||
s->build_args ? s->build_args : "");
|
||||
if (props) {
|
||||
cbm_gbuf_insert_edge(b->ctx->gbuf, img, mod->id, "DEPENDS_ON", props);
|
||||
b->edges++;
|
||||
}
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
@@ -1599,6 +1948,41 @@ static void dk_compose_connects(dk_compose_t *c, const cbm_compose_service_t *s,
|
||||
* DEFINES; it DEPENDS_ON the services it depends on / links, USAGE
|
||||
* {ref:image} its image, DEPENDS_ON {via:build} the Dockerfile it builds and
|
||||
* CONNECTS_TO the services its environment addresses. */
|
||||
/* The image a service runs: USAGE {ref: image, image[, default_of]} -- the
|
||||
* variables whose inline default named it. When the project's .env replaced
|
||||
* a default, the edge carries {override_of, override_source: ".env"}, and
|
||||
* the image the defaults alone name is kept: on the same Image as
|
||||
* `default_image`, else its own USAGE {ref: image_default, default_of}, a
|
||||
* reference the service does not run. */
|
||||
static void dk_compose_image(dk_compose_t *c, const cbm_compose_service_t *s, int64_t self) {
|
||||
int64_t img = cbm_pipeline_image_node(c->ctx, s->image);
|
||||
int64_t dimg = s->default_image ? cbm_pipeline_image_node(c->ctx, s->default_image) : 0;
|
||||
char esc[CBM_SZ_1K];
|
||||
char desc[CBM_SZ_1K];
|
||||
cbm_json_escape(esc, (int)sizeof(esc), s->image);
|
||||
cbm_json_escape(desc, (int)sizeof(desc), s->default_image ? s->default_image : "");
|
||||
const char *props =
|
||||
cbm_arena_sprintf(c->a, "{\"ref\":\"image\",\"image\":\"%s\"%s%s%s%s%s%s%s}", esc,
|
||||
s->image_default_of ? ",\"default_of\":" : "",
|
||||
s->image_default_of ? s->image_default_of : "",
|
||||
s->image_override_of ? ",\"override_of\":" : "",
|
||||
s->image_override_of ? s->image_override_of : "",
|
||||
s->image_override_of ? ",\"override_source\":\".env\"" : "",
|
||||
dimg > 0 && dimg == img ? ",\"default_image\":\"" : "",
|
||||
dimg > 0 && dimg == img ? cbm_arena_sprintf(c->a, "%s\"", desc) : "");
|
||||
if (props) {
|
||||
dk_compose_edge(c, self, img, "USAGE", props);
|
||||
}
|
||||
if (dimg > 0 && dimg != img && s->image_override_of) {
|
||||
const char *dprops = cbm_arena_sprintf(
|
||||
c->a, "{\"ref\":\"image_default\",\"image\":\"%s\",\"default_of\":%s}", desc,
|
||||
s->image_override_of);
|
||||
if (dprops) {
|
||||
dk_compose_edge(c, self, dimg, "USAGE", dprops);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void dk_compose_service(const cbm_compose_service_t *s, void *ud) {
|
||||
dk_compose_t *c = ud;
|
||||
const char *qn = dk_compose_qn(c, s->name);
|
||||
@@ -1635,11 +2019,7 @@ static void dk_compose_service(const cbm_compose_service_t *s, void *ud) {
|
||||
}
|
||||
dk_compose_connects(c, s, self->id);
|
||||
if (s->image) {
|
||||
char esc[CBM_SZ_1K];
|
||||
cbm_json_escape(esc, (int)sizeof(esc), s->image);
|
||||
char props[CBM_SZ_2K];
|
||||
snprintf(props, sizeof(props), "{\"ref\":\"image\",\"image\":\"%s\"}", esc);
|
||||
dk_compose_edge(c, self->id, cbm_pipeline_image_node(c->ctx, s->image), "USAGE", props);
|
||||
dk_compose_image(c, s, self->id);
|
||||
}
|
||||
if (s->dockerfile) {
|
||||
const char *mqn = cbm_fqn_module(c->a, c->ctx->project_name, s->dockerfile);
|
||||
@@ -1706,6 +2086,152 @@ static int dk_link_builds(cbm_pipeline_ctx_t *ctx, CBMArena *a) {
|
||||
return b.edges;
|
||||
}
|
||||
|
||||
static bool dk_prop(const char *props, const char *key, char *out, size_t out_sz);
|
||||
static void *dk_grow(CBMArena *a, void *v, int n, int *cap, size_t size);
|
||||
|
||||
/* A build that passes build arguments (compose build.args, docker build
|
||||
* --build-arg; on its build edge as `build_args`). */
|
||||
typedef struct {
|
||||
int64_t img;
|
||||
int64_t module;
|
||||
const char *dockerfile;
|
||||
const dk_var_t *args;
|
||||
const char *props; /* "tool":..,"file":..,"build_line":N */
|
||||
} dk_argbuild_t;
|
||||
|
||||
/* The build arguments of build edge properties `props` (NULL: none). */
|
||||
static const dk_var_t *dk_build_args_of(CBMArena *a, const char *props) {
|
||||
if (!props || !strstr(props, "\"build_args\"")) {
|
||||
return NULL;
|
||||
}
|
||||
yyjson_doc *doc = yyjson_read(props, strlen(props), 0);
|
||||
yyjson_val *args = doc ? yyjson_obj_get(yyjson_doc_get_root(doc), "build_args") : NULL;
|
||||
dk_var_t *list = NULL;
|
||||
yyjson_obj_iter it;
|
||||
yyjson_val *k;
|
||||
if (yyjson_is_obj(args)) {
|
||||
yyjson_obj_iter_init(args, &it);
|
||||
while ((k = yyjson_obj_iter_next(&it))) {
|
||||
const char *v = yyjson_get_str(yyjson_obj_iter_get_val(k));
|
||||
dk_var_t *var = v ? cbm_arena_alloc(a, sizeof(*var)) : NULL;
|
||||
if (var) {
|
||||
*var = (dk_var_t){.name = cbm_arena_strdup(a, yyjson_get_str(k)),
|
||||
.value = cbm_arena_strdup(a, v),
|
||||
.arg = true,
|
||||
.next = list};
|
||||
list = var;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (doc) {
|
||||
yyjson_doc_free(doc);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/* Every build edge (Image -DEPENDS_ON {via: build}-> Dockerfile) that passes
|
||||
* build arguments, into out (arena); returns how many. */
|
||||
static int dk_argbuilds(cbm_pipeline_ctx_t *ctx, CBMArena *a, dk_argbuild_t **out) {
|
||||
const cbm_gbuf_node_t **imgs = NULL;
|
||||
int ni = 0;
|
||||
*out = NULL;
|
||||
if (cbm_gbuf_find_by_label(ctx->gbuf, "Image", &imgs, &ni) != 0) {
|
||||
return 0;
|
||||
}
|
||||
int n = 0;
|
||||
int cap = 0;
|
||||
for (int i = 0; i < ni; i++) {
|
||||
const cbm_gbuf_edge_t **es = NULL;
|
||||
int ne = 0;
|
||||
if (cbm_gbuf_find_edges_by_source_type(ctx->gbuf, imgs[i]->id, "DEPENDS_ON", &es, &ne) !=
|
||||
0) {
|
||||
continue;
|
||||
}
|
||||
for (int j = 0; j < ne; j++) {
|
||||
const char *p = es[j]->properties_json;
|
||||
char via[CBM_SZ_32];
|
||||
const dk_var_t *args = NULL;
|
||||
if (!dk_prop(p, "via", via, sizeof(via)) || strcmp(via, "build") != 0 ||
|
||||
!(args = dk_build_args_of(a, p))) {
|
||||
continue;
|
||||
}
|
||||
const cbm_gbuf_node_t *mod = cbm_gbuf_find_by_id(ctx->gbuf, es[j]->target_id);
|
||||
char tool[CBM_SZ_64];
|
||||
char file[CBM_SZ_1K];
|
||||
char esc[CBM_SZ_2K];
|
||||
if (!mod || !mod->file_path || !dk_prop(p, "tool", tool, sizeof(tool)) ||
|
||||
!dk_prop(p, "file", file, sizeof(file))) {
|
||||
continue;
|
||||
}
|
||||
const char *line = strstr(p, "\"line\":");
|
||||
if (n == cap) {
|
||||
*out = dk_grow(a, *out, n, &cap, sizeof(**out));
|
||||
if (!*out) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
cbm_json_escape(esc, (int)sizeof(esc), file);
|
||||
(*out)[n++] = (dk_argbuild_t){
|
||||
.img = imgs[i]->id,
|
||||
.module = mod->id,
|
||||
.dockerfile = cbm_arena_strdup(a, mod->file_path),
|
||||
.args = args,
|
||||
.props = cbm_arena_sprintf(a, "\"tool\":\"%s\",\"file\":\"%s\",\"build_line\":%d",
|
||||
tool, esc, line ? atoi(line + strlen("\"line\":")) : 0)};
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Each build passing build arguments reads its Dockerfile in its own view
|
||||
* (dk_file_build): what those arguments change, it builds FROM. Image-sourced
|
||||
* DEPENDS_ON edges, recomputed whole every run (CBM_BUILD_EDGE_SOURCE). */
|
||||
static int dk_link_build_args(cbm_pipeline_ctx_t *ctx, CBMArena *a) {
|
||||
dk_argbuild_t *bs = NULL;
|
||||
int n = ctx->repo_path ? dk_argbuilds(ctx, a, &bs) : 0;
|
||||
if (n == 0) {
|
||||
return 0;
|
||||
}
|
||||
TSParser *parser = ts_parser_new();
|
||||
if (!parser || !ts_parser_set_language(parser, cbm_ts_language(CBM_LANG_DOCKERFILE))) {
|
||||
if (parser) {
|
||||
ts_parser_delete(parser);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int edges = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
char path[CBM_SZ_4K];
|
||||
snprintf(path, sizeof(path), "%s/%s", ctx->repo_path, bs[i].dockerfile);
|
||||
size_t len = 0;
|
||||
char *src = dk_read(path, &len);
|
||||
if (!src) {
|
||||
continue;
|
||||
}
|
||||
dk_uncomment(src, len);
|
||||
TSTree *tree = ts_parser_parse_string(parser, NULL, src, (uint32_t)len);
|
||||
if (tree) {
|
||||
dk_t d = {.ctx = ctx,
|
||||
.a = a,
|
||||
.src = src,
|
||||
.file = bs[i].dockerfile,
|
||||
.module = bs[i].module,
|
||||
.stage = -1,
|
||||
.dir = "",
|
||||
.workdir = "/",
|
||||
.build_args = bs[i].args,
|
||||
.built = bs[i].img,
|
||||
.build_props = bs[i].props};
|
||||
dk_file_build(&d, ts_tree_root_node(tree));
|
||||
edges += d.edges;
|
||||
ts_tree_delete(tree);
|
||||
}
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, src);
|
||||
}
|
||||
ts_parser_delete(parser);
|
||||
return edges;
|
||||
}
|
||||
|
||||
static bool dk_node_before(const cbm_gbuf_node_t *a, const cbm_gbuf_node_t *b) {
|
||||
int c = strcmp(a->file_path, b->file_path);
|
||||
return c < 0 || (c == 0 && a->start_line < b->start_line);
|
||||
@@ -1891,17 +2417,19 @@ static void dk_hub_push(CBMArena *a, dk_hub_edges_t *l, const cbm_gbuf_edge_t *e
|
||||
* USAGE {ref:image}, or DEPENDS_ON {via:from|copy-from}? */
|
||||
static bool dk_hub_ref(const char *type, const char *p) {
|
||||
char v[CBM_SZ_32];
|
||||
if (strcmp(type, "USAGE") == 0) {
|
||||
if (strcmp(type, "USAGE") == 0 || strcmp(type, "DEPLOYS") == 0) {
|
||||
return dk_prop(p, "ref", v, sizeof(v)) && strcmp(v, "image") == 0;
|
||||
}
|
||||
return dk_prop(p, "via", v, sizeof(v)) &&
|
||||
(strcmp(v, "from") == 0 || strcmp(v, "copy-from") == 0);
|
||||
}
|
||||
|
||||
/* The edges of Image `img`: build edges out (DEPENDS_ON {via:build}) and
|
||||
* references in, each with the tag of its `image` reference. */
|
||||
/* The edges of Image `img`: build edges out (DEPENDS_ON {via:build}), the
|
||||
* base images a build's arguments chose out (DEPENDS_ON {via: from |
|
||||
* copy-from, build_line}: they follow their build) and references in, each
|
||||
* with the tag of its `image` reference. */
|
||||
static void dk_hub_collect(cbm_pipeline_ctx_t *ctx, CBMArena *a, int64_t img,
|
||||
dk_hub_edges_t *builds, dk_hub_edges_t *refs) {
|
||||
dk_hub_edges_t *builds, dk_hub_edges_t *refs, dk_hub_edges_t *follows) {
|
||||
const cbm_gbuf_edge_t **es = NULL;
|
||||
int n = 0;
|
||||
char ref[CBM_SZ_1K];
|
||||
@@ -1909,8 +2437,13 @@ static void dk_hub_collect(cbm_pipeline_ctx_t *ctx, CBMArena *a, int64_t img,
|
||||
if (cbm_gbuf_find_edges_by_source_type(ctx->gbuf, img, "DEPENDS_ON", &es, &n) == 0) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
char via[CBM_SZ_32];
|
||||
if (!dk_prop(es[i]->properties_json, "via", via, sizeof(via)) ||
|
||||
strcmp(via, "build") != 0) {
|
||||
if (!dk_prop(es[i]->properties_json, "via", via, sizeof(via))) {
|
||||
continue;
|
||||
}
|
||||
if (strcmp(via, "build") != 0) {
|
||||
if (strstr(es[i]->properties_json, "\"build_line\":")) {
|
||||
dk_hub_push(a, follows, es[i], "");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
tag[0] = '\0';
|
||||
@@ -1921,7 +2454,7 @@ static void dk_hub_collect(cbm_pipeline_ctx_t *ctx, CBMArena *a, int64_t img,
|
||||
}
|
||||
atomic_fetch_add_explicit(&g_dk_hub_edges, (uint64_t)n, memory_order_relaxed);
|
||||
}
|
||||
static const char *const REF_TYPES[] = {"USAGE", "DEPENDS_ON"};
|
||||
static const char *const REF_TYPES[] = {"USAGE", "DEPENDS_ON", "DEPLOYS"};
|
||||
for (size_t t = 0; t < sizeof(REF_TYPES) / sizeof(REF_TYPES[0]); t++) {
|
||||
if (cbm_gbuf_find_edges_by_target_type(ctx->gbuf, img, REF_TYPES[t], &es, &n) != 0) {
|
||||
continue;
|
||||
@@ -2122,6 +2655,27 @@ static int dk_hub_move(cbm_pipeline_ctx_t *ctx, CBMArena *a, const dk_hub_edge_t
|
||||
return SKIP_ONE;
|
||||
}
|
||||
|
||||
static int dk_prop_int(const char *props, const char *key) {
|
||||
char pat[CBM_SZ_64];
|
||||
snprintf(pat, sizeof(pat), "\"%s\":", key);
|
||||
const char *p = props ? strstr(props, pat) : NULL;
|
||||
return p ? atoi(p + strlen(pat)) : -SKIP_ONE;
|
||||
}
|
||||
|
||||
/* Does base-image edge `f` come from build edge `b`: one source, and its
|
||||
* tool / file / build_line those of the build? */
|
||||
static bool dk_hub_follows(const dk_hub_edge_t *f, const dk_hub_edge_t *b) {
|
||||
char ft[CBM_SZ_64];
|
||||
char bt[CBM_SZ_64];
|
||||
char ff[CBM_SZ_1K];
|
||||
char bf[CBM_SZ_1K];
|
||||
return f->src == b->src && dk_prop(f->props, "tool", ft, sizeof(ft)) &&
|
||||
dk_prop(b->props, "tool", bt, sizeof(bt)) && strcmp(ft, bt) == 0 &&
|
||||
dk_prop(f->props, "file", ff, sizeof(ff)) && dk_prop(b->props, "file", bf, sizeof(bf)) &&
|
||||
strcmp(ff, bf) == 0 &&
|
||||
dk_prop_int(f->props, "build_line") == dk_prop_int(b->props, "line");
|
||||
}
|
||||
|
||||
/* One repository's Images (imgs[0..n)): split it by component when it is
|
||||
* built from several Dockerfiles, else keep (or bring back) everything on
|
||||
* the repository Image. Returns the edges moved. */
|
||||
@@ -2129,10 +2683,11 @@ static int dk_hub_repo(cbm_pipeline_ctx_t *ctx, CBMArena *a, const dk_img_t *img
|
||||
dk_drop_t *drop) {
|
||||
dk_hub_edges_t builds = {0};
|
||||
dk_hub_edges_t refs = {0};
|
||||
dk_hub_edges_t follows = {0};
|
||||
bool has_sub = false;
|
||||
for (int i = 0; i < n; i++) {
|
||||
has_sub = has_sub || strcmp(imgs[i].name, imgs[i].repo) != 0;
|
||||
dk_hub_collect(ctx, a, imgs[i].id, &builds, &refs);
|
||||
dk_hub_collect(ctx, a, imgs[i].id, &builds, &refs, &follows);
|
||||
}
|
||||
const char **comps = NULL;
|
||||
size_t pre = 0;
|
||||
@@ -2158,6 +2713,11 @@ static int dk_hub_repo(cbm_pipeline_ctx_t *ctx, CBMArena *a, const dk_img_t *img
|
||||
int k = builds.v[i].comp;
|
||||
int64_t src = sub && k >= 0 && sub[k] > 0 ? sub[k] : repo_img;
|
||||
moved += dk_hub_move(ctx, a, &builds.v[i], src, builds.v[i].tgt, drop);
|
||||
for (int f = 0; f < follows.n; f++) {
|
||||
if (dk_hub_follows(&follows.v[f], &builds.v[i])) {
|
||||
moved += dk_hub_move(ctx, a, &follows.v[f], src, follows.v[f].tgt, drop);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < refs.n; i++) {
|
||||
int k = sub ? dk_hub_match(refs.v[i].tag, comps, nc) : -1;
|
||||
@@ -2274,6 +2834,7 @@ void cbm_pipeline_pass_docker(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *fi
|
||||
cbm_ht_free(ix.dirs);
|
||||
}
|
||||
edges += dk_link_builds(ctx, &arena);
|
||||
edges += dk_link_build_args(ctx, &arena);
|
||||
edges += dk_image_hubs(ctx, &arena);
|
||||
cbm_arena_destroy(&arena);
|
||||
if (edges > 0) {
|
||||
|
||||
+1420
-50
File diff suppressed because it is too large.
Load diff
+154
-18
@@ -376,10 +376,12 @@ typedef struct {
|
||||
const char *ci; /* static: the CI running the command; NULL: none */
|
||||
const char *job; /* pass-arena: its job; NULL: none */
|
||||
bool chart;
|
||||
bool spawn; /* SPAWNS the script `tgt` / the Process `ext`, not DEPLOYS */
|
||||
bool make; /* SPAWNS the rule of target `ext` that makefile `path` reads */
|
||||
bool build; /* the image `ext` is built from the Dockerfile `path` */
|
||||
bool http; /* HTTP_CALLS the URL `ext` (scheme://host/path) with method `path` */
|
||||
bool spawn; /* SPAWNS the script `tgt` / the Process `ext`, not DEPLOYS */
|
||||
bool make; /* SPAWNS the rule of target `ext` that makefile `path` reads */
|
||||
bool build; /* the image `ext` is built from the Dockerfile `path` */
|
||||
bool http; /* HTTP_CALLS the URL `ext` (scheme://host/path) with method `path` */
|
||||
const char *build_args; /* pass-arena, build: --build-arg as a JSON object; NULL: none */
|
||||
const char *default_of; /* pass-arena, build: JSON array of the ${X:-d} defaults naming `ext` */
|
||||
} sh_hit_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -413,9 +415,10 @@ typedef struct {
|
||||
const char *s; /* scalar value; NULL: unknown */
|
||||
const char **items; /* array items (an item NULL: unknown); NULL: no array */
|
||||
int n;
|
||||
const char *dflt_of; /* JSON array: the variables whose ${X:-d} default made it */
|
||||
} sh_val_t;
|
||||
|
||||
static const sh_val_t SH_UNKNOWN_VAL = {NULL, NULL, 0};
|
||||
static const sh_val_t SH_UNKNOWN_VAL = {NULL, NULL, 0, NULL};
|
||||
|
||||
typedef enum { SH_FREE, SH_AT, SH_LOST } sh_cwd_mode_t;
|
||||
|
||||
@@ -464,8 +467,38 @@ typedef struct {
|
||||
sh_cwd_t *dirs; /* pushd stack */
|
||||
int ndirs;
|
||||
int cap_dirs;
|
||||
/* While set, an evaluation adds to dflt_acc (a JSON array) the variables
|
||||
* whose ${X:-d} default supplied its value. */
|
||||
bool dflt_track;
|
||||
const char *dflt_acc;
|
||||
} sh_t;
|
||||
|
||||
/* JSON array `list` (NULL: empty) with identifier `name` appended once. */
|
||||
static const char *sh_names_add(CBMArena *a, const char *list, const char *name, size_t nl) {
|
||||
if (!list) {
|
||||
return cbm_arena_sprintf(a, "[\"%.*s\"]", (int)nl, name);
|
||||
}
|
||||
char quoted[CBM_SZ_256];
|
||||
snprintf(quoted, sizeof(quoted), "\"%.*s\"", (int)nl, name);
|
||||
if (strstr(list, quoted)) {
|
||||
return list;
|
||||
}
|
||||
return cbm_arena_sprintf(a, "%.*s,%s]", (int)(strlen(list) - SKIP_ONE), list, quoted);
|
||||
}
|
||||
|
||||
/* JSON array `a` with the names of JSON array `b` appended once. */
|
||||
static const char *sh_names_union(CBMArena *ar, const char *a, const char *b) {
|
||||
for (const char *p = b ? strchr(b, '"') : NULL; p;) {
|
||||
const char *e = strchr(p + SKIP_ONE, '"');
|
||||
if (!e) {
|
||||
break;
|
||||
}
|
||||
a = sh_names_add(ar, a, p + SKIP_ONE, (size_t)(e - p - SKIP_ONE));
|
||||
p = strchr(e + SKIP_ONE, '"');
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
static void sh_set(sh_t *s, const char *name, const sh_val_t *v, bool local) {
|
||||
if (!sh_grow((void **)&s->undo, &s->cap_undo, s->nundo, sizeof(*s->undo))) {
|
||||
return;
|
||||
@@ -622,14 +655,14 @@ static const sh_val_t *sh_lookup(sh_t *s, const char *name, bool *found) {
|
||||
}
|
||||
sh_val_t *v = cbm_arena_alloc(s->a, sizeof(*v));
|
||||
if (v) {
|
||||
*v = (sh_val_t){s->script, NULL, 0};
|
||||
*v = (sh_val_t){s->script, NULL, 0, NULL};
|
||||
}
|
||||
return v ? v : &SH_UNKNOWN_VAL;
|
||||
}
|
||||
if (strcmp(name, "PWD") == 0) {
|
||||
sh_val_t *v = cbm_arena_alloc(s->a, sizeof(*v));
|
||||
if (v) {
|
||||
*v = (sh_val_t){sh_cwd_value(s), NULL, 0};
|
||||
*v = (sh_val_t){sh_cwd_value(s), NULL, 0, NULL};
|
||||
}
|
||||
return v ? v : &SH_UNKNOWN_VAL;
|
||||
}
|
||||
@@ -745,20 +778,29 @@ static const char *sh_eval_expansion(sh_t *s, TSNode n) {
|
||||
val = v->n > 0 ? v->items[0] : "";
|
||||
}
|
||||
if (!op) {
|
||||
if (found && s->dflt_track) {
|
||||
s->dflt_acc = sh_names_union(s->a, s->dflt_acc, v->dflt_of);
|
||||
}
|
||||
return found ? val : NULL;
|
||||
}
|
||||
bool colon = op[0] == ':';
|
||||
bool use_default = !found || (colon && val && !val[0]);
|
||||
if (!use_default) {
|
||||
if (s->dflt_track) {
|
||||
s->dflt_acc = sh_names_union(s->a, s->dflt_acc, v->dflt_of);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
sh_str_t b = {0};
|
||||
sh_concat(s, &b, n, op_end, close, true);
|
||||
const char *d = sh_str_done(&b);
|
||||
if (s->dflt_track) {
|
||||
s->dflt_acc = sh_names_add(s->a, s->dflt_acc, nm, strlen(nm));
|
||||
}
|
||||
if (d && strchr(op, '=')) {
|
||||
sh_val_t *nv = cbm_arena_alloc(s->a, sizeof(*nv));
|
||||
if (nv) {
|
||||
*nv = (sh_val_t){d, NULL, 0};
|
||||
*nv = (sh_val_t){d, NULL, 0, sh_names_add(s->a, NULL, nm, strlen(nm))};
|
||||
sh_set(s, nm, nv, false);
|
||||
}
|
||||
}
|
||||
@@ -943,6 +985,9 @@ static const char *sh_eval(sh_t *s, TSNode n) {
|
||||
}
|
||||
bool found = false;
|
||||
const sh_val_t *val = sh_lookup(s, nm, &found);
|
||||
if (found && s->dflt_track) {
|
||||
s->dflt_acc = sh_names_union(s->a, s->dflt_acc, val->dflt_of);
|
||||
}
|
||||
return found ? val->s : NULL;
|
||||
}
|
||||
if (strcmp(t, "expansion") == 0) {
|
||||
@@ -1319,6 +1364,8 @@ static void sh_hit_push(sh_t *s, TSNode cmd, sh_hit_t x) {
|
||||
x.line = (int)ts_node_start_point(cmd).row + SKIP_ONE + s->line_base;
|
||||
x.ext = x.ext ? cbm_arena_strdup(&h->arena, x.ext) : NULL;
|
||||
x.path = x.path ? cbm_arena_strdup(&h->arena, x.path) : NULL;
|
||||
x.build_args = x.build_args ? cbm_arena_strdup(&h->arena, x.build_args) : NULL;
|
||||
x.default_of = x.default_of ? cbm_arena_strdup(&h->arena, x.default_of) : NULL;
|
||||
x.ci = s->ci;
|
||||
x.job = s->job ? cbm_arena_strdup(&h->arena, s->job) : NULL;
|
||||
h->items[h->count++] = x;
|
||||
@@ -2268,7 +2315,11 @@ static void sh_assign(sh_t *s, TSNode n, bool local) {
|
||||
if (!nm || !v) {
|
||||
return;
|
||||
}
|
||||
*v = (sh_val_t){"", NULL, 0};
|
||||
*v = (sh_val_t){"", NULL, 0, NULL};
|
||||
bool track = s->dflt_track;
|
||||
const char *acc = s->dflt_acc;
|
||||
s->dflt_track = true;
|
||||
s->dflt_acc = NULL;
|
||||
if (sh_type(value, "array")) {
|
||||
uint32_t cnt = ts_node_named_child_count(value);
|
||||
const char **items = cbm_arena_alloc(s->a, (cnt ? cnt : SKIP_ONE) * sizeof(*items));
|
||||
@@ -2282,10 +2333,13 @@ static void sh_assign(sh_t *s, TSNode n, bool local) {
|
||||
items[k++] = sh_eval_word(s, c);
|
||||
}
|
||||
}
|
||||
*v = (sh_val_t){k > 0 ? items[0] : "", items, k};
|
||||
*v = (sh_val_t){k > 0 ? items[0] : "", items, k, NULL};
|
||||
} else if (!ts_node_is_null(value)) {
|
||||
v->s = sh_eval(s, value);
|
||||
}
|
||||
v->dflt_of = s->dflt_acc;
|
||||
s->dflt_track = track;
|
||||
s->dflt_acc = acc;
|
||||
bool append = false;
|
||||
for (uint32_t i = 0; i < ts_node_child_count(n); i++) {
|
||||
TSNode c = ts_node_child(n, i);
|
||||
@@ -2434,6 +2488,64 @@ static const char *sh_image_repo(sh_t *s, TSNode cmd, const sh_argv_t *a, int i)
|
||||
return sh_str_done(&out);
|
||||
}
|
||||
|
||||
/* JSON object `json` (NULL: none) with build argument `kv` ("K=V") added; a
|
||||
* `K` alone (the environment's value), an unknown word or a secret (by name
|
||||
* or value -- never kept in the graph, never an image) adds nothing. */
|
||||
static const char *sh_build_arg(sh_t *s, const char *json, const char *kv) {
|
||||
const char *eq = kv ? strchr(kv, '=') : NULL;
|
||||
if (!eq || eq == kv) {
|
||||
return json;
|
||||
}
|
||||
for (const char *p = kv; p < eq; p++) {
|
||||
if (!isalnum((unsigned char)*p) && *p != '_') {
|
||||
return json;
|
||||
}
|
||||
}
|
||||
const char *key = cbm_arena_strndup(s->a, kv, (size_t)(eq - kv));
|
||||
if (!key || cbm_is_secret_binding(key, eq + SKIP_ONE)) {
|
||||
return json;
|
||||
}
|
||||
/* JSON escaping writes at most 6 bytes a byte: nothing is cut */
|
||||
int vn = (int)(strlen(eq + SKIP_ONE) * 6 + SKIP_ONE);
|
||||
char *ev = cbm_arena_alloc(s->a, (size_t)vn);
|
||||
if (!ev) {
|
||||
return json;
|
||||
}
|
||||
cbm_json_escape(ev, vn, eq + SKIP_ONE);
|
||||
const char *pair = cbm_arena_sprintf(s->a, "\"%.*s\":\"%s\"", (int)(eq - kv), kv, ev);
|
||||
if (!pair) {
|
||||
return json;
|
||||
}
|
||||
return json ? cbm_arena_sprintf(s->a, "%.*s,%s}", (int)(strlen(json) - SKIP_ONE), json, pair)
|
||||
: cbm_arena_sprintf(s->a, "{%s}", pair);
|
||||
}
|
||||
|
||||
/* The variables whose ${X:-d} default supplied argv word `i` (a JSON array,
|
||||
* NULL: none): the word evaluated once more with the defaults recorded. */
|
||||
static const char *sh_word_defaults(sh_t *s, TSNode cmd, const sh_argv_t *a, int i) {
|
||||
uint32_t b = a->span[(size_t)i * PAIR_LEN];
|
||||
uint32_t e = a->span[(size_t)i * PAIR_LEN + SKIP_ONE];
|
||||
if (!memchr(s->src + b, '$', e - b)) {
|
||||
return NULL;
|
||||
}
|
||||
TSNode w = ts_node_descendant_for_byte_range(cmd, b, e);
|
||||
while (!ts_node_is_null(w) && (ts_node_start_byte(w) != b || ts_node_end_byte(w) != e)) {
|
||||
w = ts_node_parent(w);
|
||||
}
|
||||
if (ts_node_is_null(w)) {
|
||||
return NULL;
|
||||
}
|
||||
bool track = s->dflt_track;
|
||||
const char *acc = s->dflt_acc;
|
||||
s->dflt_track = true;
|
||||
s->dflt_acc = NULL;
|
||||
(void)sh_eval_word(s, w);
|
||||
const char *out = s->dflt_acc;
|
||||
s->dflt_track = track;
|
||||
s->dflt_acc = acc;
|
||||
return out;
|
||||
}
|
||||
|
||||
/* docker / podman / nerdctl [buildx | image | builder] build, buildah bud:
|
||||
* each tagged image is built from the Dockerfile -f names (against the cwd),
|
||||
* else <context>/Dockerfile -- one file of the repository. */
|
||||
@@ -2467,17 +2579,20 @@ static void sh_build(sh_t *s, TSNode cmd, const sh_argv_t *a) {
|
||||
cbm_ht_set(s->hits->declarers, s->file, (void *)s->file);
|
||||
}
|
||||
int tags[SH_BUILD_TAGS];
|
||||
int tag_words[SH_BUILD_TAGS];
|
||||
const char *tag_vals[SH_BUILD_TAGS];
|
||||
int nt = 0;
|
||||
const char *dfile = NULL;
|
||||
bool dfile_known = true;
|
||||
const char *ctx = NULL;
|
||||
bool ctx_seen = false;
|
||||
const char *args = NULL;
|
||||
for (; i < a->n; i++) {
|
||||
const char *w = a->v[i];
|
||||
if (w && (sh_eq(w, "-t") || sh_eq(w, "--tag")) && i + SKIP_ONE < a->n) {
|
||||
if (nt < SH_BUILD_TAGS) {
|
||||
tags[nt] = i + SKIP_ONE;
|
||||
tag_words[nt] = i + SKIP_ONE;
|
||||
tag_vals[nt++] = a->v[i + SKIP_ONE];
|
||||
}
|
||||
i++;
|
||||
@@ -2485,9 +2600,15 @@ static void sh_build(sh_t *s, TSNode cmd, const sh_argv_t *a) {
|
||||
dfile = a->v[i + SKIP_ONE];
|
||||
dfile_known = dfile != NULL;
|
||||
i++;
|
||||
} else if (w && sh_eq(w, "--build-arg") && i + SKIP_ONE < a->n) {
|
||||
args = sh_build_arg(s, args, a->v[i + SKIP_ONE]);
|
||||
i++;
|
||||
} else if (w && strncmp(w, "--build-arg=", strlen("--build-arg=")) == 0) {
|
||||
args = sh_build_arg(s, args, w + strlen("--build-arg="));
|
||||
} else if (w && strncmp(w, "--tag=", strlen("--tag=")) == 0) {
|
||||
if (nt < SH_BUILD_TAGS && w[strlen("--tag=")]) {
|
||||
tags[nt] = -SKIP_ONE;
|
||||
tag_words[nt] = i;
|
||||
tag_vals[nt++] = w + strlen("--tag=");
|
||||
}
|
||||
} else if (w && strncmp(w, "--file=", strlen("--file=")) == 0) {
|
||||
@@ -2513,7 +2634,12 @@ static void sh_build(sh_t *s, TSNode cmd, const sh_argv_t *a) {
|
||||
}
|
||||
if (img && img[0]) {
|
||||
sh_hit_push(s, cmd,
|
||||
(sh_hit_t){.via = "docker-build", .ext = img, .path = hit, .build = true});
|
||||
(sh_hit_t){.via = "docker-build",
|
||||
.ext = img,
|
||||
.path = hit,
|
||||
.build = true,
|
||||
.build_args = args,
|
||||
.default_of = sh_word_defaults(s, cmd, a, tag_words[k])});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3305,8 +3431,8 @@ static void sh_makefile(sh_pass_t *p, const char *rel, const char *src, size_t l
|
||||
sh_val_t *root = cbm_arena_alloc(&a, sizeof(*root));
|
||||
sh_val_t *cur = cbm_arena_alloc(&a, sizeof(*cur));
|
||||
if (root && cur) {
|
||||
*root = (sh_val_t){SH_ROOT, NULL, 0};
|
||||
*cur = (sh_val_t){sh_anchor(&a, s.sdir), NULL, 0};
|
||||
*root = (sh_val_t){SH_ROOT, NULL, 0, NULL};
|
||||
*cur = (sh_val_t){sh_anchor(&a, s.sdir), NULL, 0, NULL};
|
||||
cbm_ht_set(s.env, "__cbm_root", root);
|
||||
if (m.makefile) {
|
||||
cbm_ht_set(s.env, "__cbm_curdir", cur);
|
||||
@@ -3647,15 +3773,25 @@ static int sh_link_build(cbm_pipeline_ctx_t *ctx, const sh_hit_t *x, CBMHashTabl
|
||||
char esc_file[CBM_SZ_1K];
|
||||
char esc_img[CBM_SZ_1K];
|
||||
char ci[CBM_SZ_1K];
|
||||
char props[CBM_SZ_4K];
|
||||
cbm_json_escape(esc_file, (int)sizeof(esc_file), x->file);
|
||||
cbm_json_escape(esc_img, (int)sizeof(esc_img), x->ext);
|
||||
sh_ci_props(x, ci, sizeof(ci));
|
||||
snprintf(props, sizeof(props),
|
||||
/* sized to what it holds: default_of and build_args are never cut */
|
||||
size_t pn = strlen(esc_file) + strlen(esc_img) + strlen(ci) +
|
||||
(x->default_of ? strlen(x->default_of) : 0) +
|
||||
(x->build_args ? strlen(x->build_args) : 0) + CBM_SZ_256;
|
||||
char *props = cbm_alloc(CBM_MEM_CLASS_OTHER, pn);
|
||||
if (!props) {
|
||||
return 0;
|
||||
}
|
||||
snprintf(props, pn,
|
||||
"{\"via\":\"build\",\"tool\":\"docker-build\",\"file\":\"%s\",\"image\":\"%s\"%s,"
|
||||
"\"line\":%d}",
|
||||
esc_file, esc_img, ci, x->line);
|
||||
"\"line\":%d%s%s%s%s}",
|
||||
esc_file, esc_img, ci, x->line, x->default_of ? ",\"default_of\":" : "",
|
||||
x->default_of ? x->default_of : "", x->build_args ? ",\"build_args\":" : "",
|
||||
x->build_args ? x->build_args : "");
|
||||
cbm_gbuf_insert_edge(ctx->gbuf, img, mod, "DEPENDS_ON", props);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, props);
|
||||
return SKIP_ONE;
|
||||
}
|
||||
|
||||
@@ -3852,7 +3988,7 @@ typedef struct {
|
||||
static void sh_ci_var(sh_t *s, const char *name, const char *value) {
|
||||
sh_val_t *v = value ? cbm_arena_alloc(s->a, sizeof(*v)) : NULL;
|
||||
if (v) {
|
||||
*v = (sh_val_t){value, NULL, 0};
|
||||
*v = (sh_val_t){value, NULL, 0, NULL};
|
||||
}
|
||||
sh_set(s, name, v ? v : &SH_UNKNOWN_VAL, false);
|
||||
}
|
||||
|
||||
@@ -22,13 +22,16 @@
|
||||
#include "foundation/log.h"
|
||||
#include "foundation/mem_core.h"
|
||||
#include "foundation/str_util.h"
|
||||
#include "foundation/json_writer.h" /* cbm_json_writer_allocator */
|
||||
#include "graph_buffer/graph_buffer.h"
|
||||
#include "pipeline/helm_template.h"
|
||||
#include "pipeline/pipeline.h"
|
||||
#include "pipeline/pipeline_internal.h"
|
||||
#include "cbm.h"
|
||||
#include "helpers.h"
|
||||
#include "lang_specs.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
@@ -249,6 +252,522 @@ static bool tf_plain_name(const char *s) {
|
||||
}
|
||||
|
||||
/* One `resource "helm_release" "<name>" { ... }` block. */
|
||||
/* ── helm_release values (for pass_k8s.c's release images) ────────── */
|
||||
|
||||
/* The expression inside wrapper nodes. */
|
||||
static TSNode tf_unwrap(TSNode n) {
|
||||
while (tf_type(n, "expression") || tf_type(n, "literal_value") ||
|
||||
tf_type(n, "collection_value") || tf_type(n, "template_expr")) {
|
||||
if (ts_node_named_child_count(n) != SKIP_ONE) {
|
||||
break;
|
||||
}
|
||||
n = ts_node_named_child(n, 0);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* A heredoc's text (a `<<-` one dedented); NULL when it interpolates. */
|
||||
static char *tf_heredoc(const tf_t *t, TSNode n) {
|
||||
TSNode open = {0};
|
||||
TSNode close = {0};
|
||||
bool dedent = false;
|
||||
for (uint32_t i = 0; i < ts_node_named_child_count(n); i++) {
|
||||
TSNode c = ts_node_named_child(n, i);
|
||||
if (tf_type(c, "heredoc_start")) {
|
||||
uint32_t b = ts_node_start_byte(c);
|
||||
dedent = ts_node_end_byte(c) - b >= 3 && t->src[b + PAIR_LEN] == '-';
|
||||
} else if (tf_type(c, "heredoc_identifier")) {
|
||||
if (ts_node_is_null(open)) {
|
||||
open = c;
|
||||
} else {
|
||||
close = c;
|
||||
}
|
||||
} else if (!tf_type(c, "template_literal")) {
|
||||
return NULL; /* an interpolation or a directive: no literal */
|
||||
}
|
||||
}
|
||||
if (ts_node_is_null(open) || ts_node_is_null(close)) {
|
||||
return NULL;
|
||||
}
|
||||
const char *b = t->src + ts_node_end_byte(open);
|
||||
const char *e = t->src + ts_node_start_byte(close);
|
||||
const char *nl = memchr(b, '\n', (size_t)(e - b));
|
||||
b = nl ? nl + SKIP_ONE : e;
|
||||
while (e > b && e[-1] != '\n') {
|
||||
e--; /* the closing line's indentation */
|
||||
}
|
||||
size_t indent = (size_t)-1;
|
||||
for (const char *p = b; dedent && p < e;) {
|
||||
const char *le = memchr(p, '\n', (size_t)(e - p));
|
||||
le = le ? le : e;
|
||||
size_t sp = 0;
|
||||
while (p + sp < le && (p[sp] == ' ' || p[sp] == '\t')) {
|
||||
sp++;
|
||||
}
|
||||
if (p + sp < le && sp < indent) {
|
||||
indent = sp;
|
||||
}
|
||||
p = le + (le < e ? SKIP_ONE : 0);
|
||||
}
|
||||
if (!dedent || indent == (size_t)-1) {
|
||||
indent = 0;
|
||||
}
|
||||
char *out = cbm_alloc(CBM_MEM_CLASS_OTHER, (size_t)(e - b) + SKIP_ONE);
|
||||
size_t k = 0;
|
||||
for (const char *p = b; out && p < e;) {
|
||||
const char *le = memchr(p, '\n', (size_t)(e - p));
|
||||
le = le ? le + SKIP_ONE : e;
|
||||
size_t skip = 0;
|
||||
while (skip < indent && p + skip < le && (p[skip] == ' ' || p[skip] == '\t')) {
|
||||
skip++;
|
||||
}
|
||||
memcpy(out + k, p + skip, (size_t)(le - p) - skip);
|
||||
k += (size_t)(le - p) - skip;
|
||||
p = le;
|
||||
}
|
||||
if (out) {
|
||||
out[k] = '\0';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/* HCL number literal s (digits, a fraction, an exponent) as Terraform turns a
|
||||
* number into a string: exact decimal, no exponent, no trailing zeros (cty's
|
||||
* big.Float Text('f', -1); 1.10 -> "1.1", 2e3 -> "2000"). False when it is
|
||||
* no such literal or does not fit out. */
|
||||
static bool tf_number_text(const char *s, size_t n, char *out, size_t sz) {
|
||||
enum { TF_MAX_EXP = 4096 };
|
||||
char dig[CBM_SZ_256];
|
||||
size_t nd = 0;
|
||||
long exp = 0;
|
||||
bool frac = false;
|
||||
size_t i = 0;
|
||||
for (; i < n && (isdigit((unsigned char)s[i]) || s[i] == '.'); i++) {
|
||||
if (s[i] == '.') {
|
||||
if (frac) {
|
||||
return false;
|
||||
}
|
||||
frac = true;
|
||||
continue;
|
||||
}
|
||||
if (nd + SKIP_ONE >= sizeof(dig)) {
|
||||
return false;
|
||||
}
|
||||
dig[nd++] = s[i];
|
||||
exp -= frac ? SKIP_ONE : 0;
|
||||
}
|
||||
if (i < n) {
|
||||
if (s[i] != 'e' && s[i] != 'E') {
|
||||
return false;
|
||||
}
|
||||
i++;
|
||||
bool neg = i < n && s[i] == '-';
|
||||
i += i < n && (s[i] == '-' || s[i] == '+');
|
||||
long e = 0;
|
||||
if (i == n) {
|
||||
return false;
|
||||
}
|
||||
for (; i < n; i++) {
|
||||
if (!isdigit((unsigned char)s[i]) || e > TF_MAX_EXP) {
|
||||
return false;
|
||||
}
|
||||
e = e * 10 + (s[i] - '0');
|
||||
}
|
||||
exp += neg ? -e : e;
|
||||
}
|
||||
size_t b = 0;
|
||||
while (b < nd && dig[b] == '0') {
|
||||
b++;
|
||||
}
|
||||
if (b == nd) {
|
||||
return snprintf(out, sz, "0") == SKIP_ONE; /* every digit 0 (or none) */
|
||||
}
|
||||
size_t e = nd;
|
||||
while (dig[e - SKIP_ONE] == '0') { /* stops at dig[b], which is no 0 */
|
||||
e--;
|
||||
exp++;
|
||||
}
|
||||
size_t m = e - b; /* significant digits: dig[b, e) x 10^exp */
|
||||
size_t o = 0;
|
||||
long point = (long)m + exp; /* digits before the decimal point */
|
||||
size_t need = exp >= 0 ? m + (size_t)exp
|
||||
: point > 0 ? m + SKIP_ONE
|
||||
: m + PAIR_LEN + (size_t)-point;
|
||||
if (need + SKIP_ONE > sz) {
|
||||
return false;
|
||||
}
|
||||
if (point <= 0) {
|
||||
out[o++] = '0';
|
||||
out[o++] = '.';
|
||||
for (long z = 0; z < -point; z++) {
|
||||
out[o++] = '0';
|
||||
}
|
||||
}
|
||||
for (size_t k = 0; k < m; k++) {
|
||||
if (point > 0 && (long)k == point) {
|
||||
out[o++] = '.';
|
||||
}
|
||||
out[o++] = dig[b + k];
|
||||
}
|
||||
for (long z = 0; z < exp; z++) {
|
||||
out[o++] = '0';
|
||||
}
|
||||
out[o] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
/* An HCL literal as JSON: a string without interpolation, a number, a bool,
|
||||
* null, a tuple or object of those; NULL: anything else. `num_text`: a number
|
||||
* as the string Terraform makes of it (a helm_release set value is a string),
|
||||
* else a float64. */
|
||||
static yyjson_mut_val *tf_json_v(const tf_t *t, yyjson_mut_doc *doc, TSNode n, bool num_text) {
|
||||
n = tf_unwrap(n);
|
||||
if (tf_type(n, "string_lit") || tf_type(n, "quoted_template")) {
|
||||
char buf[CBM_SZ_4K];
|
||||
size_t len = 0;
|
||||
buf[0] = '\0';
|
||||
for (uint32_t i = 0; i < ts_node_named_child_count(n); i++) {
|
||||
TSNode c = ts_node_named_child(n, i);
|
||||
if (tf_type(c, "quoted_template_start") || tf_type(c, "quoted_template_end")) {
|
||||
continue;
|
||||
}
|
||||
if (!tf_type(c, "template_literal")) {
|
||||
return NULL;
|
||||
}
|
||||
uint32_t b = ts_node_start_byte(c);
|
||||
if (!tf_add(buf, sizeof(buf), &len, t->src + b, ts_node_end_byte(c) - b)) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
size_t k = 0; /* \" \\ \n \t */
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (buf[i] == '\\' && i + SKIP_ONE < len) {
|
||||
char c = buf[++i];
|
||||
buf[k++] = c == 'n' ? '\n' : c == 't' ? '\t' : c;
|
||||
} else {
|
||||
buf[k++] = buf[i];
|
||||
}
|
||||
}
|
||||
return yyjson_mut_strncpy(doc, buf, k);
|
||||
}
|
||||
if (tf_type(n, "heredoc_template")) {
|
||||
char *s = tf_heredoc(t, n);
|
||||
yyjson_mut_val *v = s ? yyjson_mut_strcpy(doc, s) : NULL;
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, s);
|
||||
return v;
|
||||
}
|
||||
uint32_t b = ts_node_start_byte(n);
|
||||
uint32_t e = ts_node_end_byte(n);
|
||||
if (tf_type(n, "numeric_lit")) {
|
||||
char buf[CBM_SZ_256];
|
||||
if (num_text) {
|
||||
return tf_number_text(t->src + b, e - b, buf, sizeof(buf)) ? yyjson_mut_strcpy(doc, buf)
|
||||
: NULL;
|
||||
}
|
||||
snprintf(buf, sizeof(buf), "%.*s", (int)(e - b), t->src + b);
|
||||
return yyjson_mut_real(doc, strtod(buf, NULL));
|
||||
}
|
||||
if (tf_type(n, "bool_lit")) {
|
||||
return yyjson_mut_bool(doc, e - b == strlen("true") && strncmp(t->src + b, "true", 4) == 0);
|
||||
}
|
||||
if (tf_type(n, "null_lit")) {
|
||||
return yyjson_mut_null(doc);
|
||||
}
|
||||
if (tf_type(n, "tuple")) {
|
||||
yyjson_mut_val *arr = yyjson_mut_arr(doc);
|
||||
for (uint32_t i = 0; arr && i < ts_node_named_child_count(n); i++) {
|
||||
TSNode c = ts_node_named_child(n, i);
|
||||
if (tf_type(c, "tuple_start") || tf_type(c, "tuple_end")) {
|
||||
continue;
|
||||
}
|
||||
yyjson_mut_val *v = tf_json_v(t, doc, c, num_text);
|
||||
if (!v) {
|
||||
return NULL;
|
||||
}
|
||||
yyjson_mut_arr_append(arr, v);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
if (tf_type(n, "object")) {
|
||||
yyjson_mut_val *obj = yyjson_mut_obj(doc);
|
||||
for (uint32_t i = 0; obj && i < ts_node_named_child_count(n); i++) {
|
||||
TSNode c = ts_node_named_child(n, i);
|
||||
if (!tf_type(c, "object_elem")) {
|
||||
continue;
|
||||
}
|
||||
TSNode kn = tf_unwrap(ts_node_child_by_field_name(c, "key", (uint32_t)strlen("key")));
|
||||
TSNode vn = ts_node_child_by_field_name(c, "val", (uint32_t)strlen("val"));
|
||||
yyjson_mut_val *key = NULL;
|
||||
if (tf_type(kn, "variable_expr") && ts_node_named_child_count(kn) == SKIP_ONE) {
|
||||
TSNode id = ts_node_named_child(kn, 0);
|
||||
uint32_t kb = ts_node_start_byte(id);
|
||||
key = yyjson_mut_strncpy(doc, t->src + kb, ts_node_end_byte(id) - kb);
|
||||
} else {
|
||||
key = tf_json_v(t, doc, kn, false);
|
||||
}
|
||||
yyjson_mut_val *val = tf_json_v(t, doc, vn, num_text);
|
||||
if (!key || !yyjson_mut_is_str(key) || !val) {
|
||||
return NULL;
|
||||
}
|
||||
(void)yyjson_mut_obj_put(obj, key, val);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static yyjson_mut_val *tf_json(const tf_t *t, yyjson_mut_doc *doc, TSNode n) {
|
||||
return tf_json_v(t, doc, n, false);
|
||||
}
|
||||
|
||||
static char *tf_read(const char *path, size_t *len);
|
||||
|
||||
/* A repository file's text (cbm_alloc; capped and padded like every file
|
||||
* read; an empty file is empty text), NULL when unreadable -- or when it is
|
||||
* no file discovery took in: a symlink, or a path through one, is never
|
||||
* followed out of the repository. */
|
||||
static char *tf_read_repo(const tf_t *t, const char *path, size_t *len) {
|
||||
if (!cbm_pipeline_file_node(t->ctx->gbuf, t->ctx->project_name, path)) {
|
||||
return NULL;
|
||||
}
|
||||
char full[CBM_SZ_4K];
|
||||
int n = snprintf(full, sizeof(full), "%s/%s", t->ctx->repo_path, path);
|
||||
if (n <= 0 || (size_t)n >= sizeof(full)) {
|
||||
return NULL;
|
||||
}
|
||||
char *buf = tf_read(full, len);
|
||||
cbm_path_info_t info;
|
||||
if (!buf && cbm_path_info_utf8(full, &info) == CBM_PATH_INFO_OK && info.is_regular &&
|
||||
!info.is_symlink && info.size == 0) {
|
||||
enum { TF_EMPTY_PAD = 16 };
|
||||
buf = cbm_calloc(CBM_MEM_CLASS_OTHER, TF_EMPTY_PAD);
|
||||
*len = 0;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* A `values = [...]` item as values: a YAML string / heredoc, file(<path>)
|
||||
* of a repository file, yamlencode / jsonencode of a literal; NULL: none of
|
||||
* those. */
|
||||
static yyjson_mut_val *tf_values_item(const tf_t *t, yyjson_mut_doc *doc, TSNode item, CBMArena *a,
|
||||
const char ***files, int *nfiles) {
|
||||
TSNode n = tf_unwrap(item);
|
||||
if (tf_type(n, "function_call")) {
|
||||
TSNode fn = ts_node_named_child(n, 0);
|
||||
TSNode args = {0};
|
||||
for (uint32_t i = 0; i < ts_node_named_child_count(n); i++) {
|
||||
if (tf_type(ts_node_named_child(n, i), "function_arguments")) {
|
||||
args = ts_node_named_child(n, i);
|
||||
}
|
||||
}
|
||||
if (ts_node_is_null(fn) || ts_node_is_null(args) ||
|
||||
ts_node_named_child_count(args) != SKIP_ONE) {
|
||||
return NULL;
|
||||
}
|
||||
uint32_t fb = ts_node_start_byte(fn);
|
||||
size_t fl = ts_node_end_byte(fn) - fb;
|
||||
TSNode arg = ts_node_named_child(args, 0);
|
||||
if ((fl == strlen("yamlencode") && strncmp(t->src + fb, "yamlencode", fl) == 0) ||
|
||||
(fl == strlen("jsonencode") && strncmp(t->src + fb, "jsonencode", fl) == 0)) {
|
||||
return tf_json(t, doc, arg);
|
||||
}
|
||||
if (fl == strlen("file") && strncmp(t->src + fb, "file", fl) == 0) {
|
||||
char v[CBM_SZ_1K];
|
||||
char path[CBM_SZ_1K];
|
||||
if (!t->ctx->repo_path || !tf_string(t->src, t->mdir, arg, v, sizeof(v)) ||
|
||||
!tf_repo_path(t->mdir, v, path, sizeof(path))) {
|
||||
return NULL;
|
||||
}
|
||||
const char **grown = cbm_arena_alloc(a, (size_t)(*nfiles + SKIP_ONE) * sizeof(*grown));
|
||||
if (grown) {
|
||||
if (*nfiles) {
|
||||
memcpy(grown, *files, (size_t)*nfiles * sizeof(*grown));
|
||||
}
|
||||
grown[(*nfiles)++] = cbm_arena_strdup(a, path);
|
||||
*files = grown;
|
||||
}
|
||||
size_t len = 0;
|
||||
char *buf = tf_read_repo(t, path, &len);
|
||||
yyjson_mut_val *out = buf ? cbm_helm_values_parse(doc, buf, len) : NULL;
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, buf);
|
||||
return out;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
yyjson_mut_val *s = tf_json(t, doc, n);
|
||||
return yyjson_mut_is_str(s)
|
||||
? cbm_helm_values_parse(doc, yyjson_mut_get_str(s), yyjson_mut_get_len(s))
|
||||
: NULL;
|
||||
}
|
||||
|
||||
/* A `set` entry (name / value / type; read with numbers as text) applied to
|
||||
* root; false when it is no literal. */
|
||||
static bool tf_set_entry(yyjson_mut_doc *doc, yyjson_mut_val *root, yyjson_mut_val *entry) {
|
||||
yyjson_mut_val *nm = yyjson_mut_obj_get(entry, "name");
|
||||
yyjson_mut_val *val = yyjson_mut_obj_get(entry, "value");
|
||||
yyjson_mut_val *ty = yyjson_mut_obj_get(entry, "type");
|
||||
if (!yyjson_mut_is_str(nm) || !val) {
|
||||
return false;
|
||||
}
|
||||
const char *v = NULL;
|
||||
if (yyjson_mut_is_str(val)) {
|
||||
v = yyjson_mut_get_str(val);
|
||||
} else if (yyjson_mut_is_bool(val)) {
|
||||
v = yyjson_mut_get_bool(val) ? "true" : "false";
|
||||
}
|
||||
bool as_string = yyjson_mut_is_str(ty) && strcmp(yyjson_mut_get_str(ty), "string") == 0;
|
||||
return v && cbm_helm_values_set(doc, root, yyjson_mut_get_str(nm), v, as_string);
|
||||
}
|
||||
|
||||
/* A `set` block's attributes as an object; NULL when one is no literal. */
|
||||
static yyjson_mut_val *tf_set_block(const tf_t *t, yyjson_mut_doc *doc, TSNode block) {
|
||||
yyjson_mut_val *entry = yyjson_mut_obj(doc);
|
||||
for (uint32_t j = 0; entry && j < ts_node_named_child_count(block); j++) {
|
||||
TSNode sb = ts_node_named_child(block, j);
|
||||
if (!tf_type(sb, "body")) {
|
||||
continue;
|
||||
}
|
||||
for (uint32_t k = 0; k < ts_node_named_child_count(sb); k++) {
|
||||
TSNode at = ts_node_named_child(sb, k);
|
||||
if (!tf_type(at, "attribute") || ts_node_named_child_count(at) < PAIR_LEN) {
|
||||
continue;
|
||||
}
|
||||
TSNode key = ts_node_named_child(at, 0);
|
||||
uint32_t b = ts_node_start_byte(key);
|
||||
yyjson_mut_val *v = tf_json_v(t, doc, ts_node_named_child(at, SKIP_ONE), true);
|
||||
if (!v) {
|
||||
return NULL;
|
||||
}
|
||||
yyjson_mut_obj_add(entry,
|
||||
yyjson_mut_strncpy(doc, t->src + b, ts_node_end_byte(key) - b), v);
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
/* Is block `a` a `dynamic "set"` / `"set_list"` / `"set_sensitive"` /
|
||||
* `"postrender"` -- made by a for_each, never literal? */
|
||||
static bool tf_dynamic_set(const tf_t *t, TSNode a) {
|
||||
TSNode kw = ts_node_named_child(a, 0);
|
||||
TSNode label = ts_node_named_child(a, SKIP_ONE);
|
||||
if (!tf_type(a, "block") || ts_node_is_null(kw) || ts_node_is_null(label)) {
|
||||
return false;
|
||||
}
|
||||
uint32_t kb = ts_node_start_byte(kw);
|
||||
if (ts_node_end_byte(kw) - kb != strlen("dynamic") ||
|
||||
strncmp(t->src + kb, "dynamic", strlen("dynamic")) != 0) {
|
||||
return false;
|
||||
}
|
||||
uint32_t lb = ts_node_start_byte(label);
|
||||
size_t ll = ts_node_end_byte(label) - lb;
|
||||
return (ll == strlen("\"set\"") && strncmp(t->src + lb, "\"set\"", ll) == 0) ||
|
||||
(ll == strlen("\"set_list\"") && strncmp(t->src + lb, "\"set_list\"", ll) == 0) ||
|
||||
(ll == strlen("\"set_sensitive\"") &&
|
||||
strncmp(t->src + lb, "\"set_sensitive\"", ll) == 0) ||
|
||||
(ll == strlen("\"postrender\"") && strncmp(t->src + lb, "\"postrender\"", ll) == 0);
|
||||
}
|
||||
|
||||
/* Is node n an identifier spelling `word`? */
|
||||
static bool tf_word(const tf_t *t, TSNode n, const char *word) {
|
||||
if (ts_node_is_null(n)) {
|
||||
return false;
|
||||
}
|
||||
uint32_t b = ts_node_start_byte(n);
|
||||
size_t l = ts_node_end_byte(n) - b;
|
||||
return l == strlen(word) && strncmp(t->src + b, word, l) == 0;
|
||||
}
|
||||
|
||||
/* The values a helm_release body gives its chart -- `values` items
|
||||
* coalesced in order, then each `set` -- as JSON (cbm_alloc); NULL when a
|
||||
* part is no literal, is made by a `dynamic` block, is sensitive (its value
|
||||
* is never read), or a postrender -- a block, an attribute or a dynamic one
|
||||
* -- may rewrite what it deploys. */
|
||||
static char *tf_release_values(const tf_t *t, TSNode body, const char ***files, int *nfiles) {
|
||||
yyjson_mut_doc *doc = yyjson_mut_doc_new(cbm_json_writer_allocator());
|
||||
yyjson_mut_val *root = doc ? yyjson_mut_obj(doc) : NULL;
|
||||
bool ok = root != NULL;
|
||||
for (int pass = 0; ok && pass < PAIR_LEN; pass++) {
|
||||
for (uint32_t i = 0; ok && i < ts_node_named_child_count(body); i++) {
|
||||
TSNode a = ts_node_named_child(body, i);
|
||||
TSNode key = ts_node_named_child(a, 0);
|
||||
uint32_t kb = ts_node_is_null(key) ? 0 : ts_node_start_byte(key);
|
||||
size_t kl = ts_node_is_null(key) ? 0 : ts_node_end_byte(key) - kb;
|
||||
bool is_set = kl == strlen("set") && strncmp(t->src + kb, "set", kl) == 0;
|
||||
bool is_values = kl == strlen("values") && strncmp(t->src + kb, "values", kl) == 0;
|
||||
/* set_list, set_sensitive and dynamic set values are not read:
|
||||
* the release's values would be partial; a postrender may
|
||||
* rewrite its images */
|
||||
if (tf_word(t, key, "set_list") || tf_word(t, key, "set_sensitive") ||
|
||||
tf_word(t, key, "postrender") || tf_dynamic_set(t, a)) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
if (pass == 0 && is_values && tf_type(a, "attribute")) {
|
||||
TSNode list = tf_unwrap(ts_node_named_child(a, SKIP_ONE));
|
||||
ok = tf_type(list, "tuple");
|
||||
for (uint32_t j = 0; ok && j < ts_node_named_child_count(list); j++) {
|
||||
TSNode it = ts_node_named_child(list, j);
|
||||
if (tf_type(it, "tuple_start") || tf_type(it, "tuple_end")) {
|
||||
continue;
|
||||
}
|
||||
yyjson_mut_val *v = tf_values_item(t, doc, it, t->arena, files, nfiles);
|
||||
ok = v && yyjson_mut_is_obj(v);
|
||||
root = ok ? cbm_helm_values_coalesce(doc, root, v) : root;
|
||||
}
|
||||
} else if (pass == SKIP_ONE && is_set && tf_type(a, "block")) {
|
||||
yyjson_mut_val *entry = tf_set_block(t, doc, a);
|
||||
ok = entry && tf_set_entry(doc, root, entry);
|
||||
} else if (pass == SKIP_ONE && is_set && tf_type(a, "attribute")) {
|
||||
/* provider v3: set = [{ name, value, type }, ...] */
|
||||
yyjson_mut_val *list = tf_json_v(t, doc, ts_node_named_child(a, SKIP_ONE), true);
|
||||
ok = yyjson_mut_is_arr(list);
|
||||
size_t idx, max;
|
||||
yyjson_mut_val *entry;
|
||||
yyjson_mut_arr_foreach(list, idx, max, entry) {
|
||||
ok = ok && tf_set_entry(doc, root, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
char *out = NULL;
|
||||
if (ok) {
|
||||
size_t len = 0;
|
||||
out = yyjson_mut_val_write_opts(root, 0, cbm_json_writer_allocator(), &len, NULL);
|
||||
}
|
||||
if (doc) {
|
||||
yyjson_mut_doc_free(doc);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static void tf_release_push(tf_t *t, int64_t src, int64_t chart, TSNode body) {
|
||||
cbm_pipeline_ctx_t *ctx = t->ctx;
|
||||
if (ctx->tf_release_n == ctx->tf_release_cap) {
|
||||
int cap = ctx->tf_release_cap ? ctx->tf_release_cap * PAIR_LEN : CBM_SZ_16;
|
||||
cbm_tf_release_t *g =
|
||||
cbm_realloc(CBM_MEM_CLASS_OTHER, ctx->tf_releases, (size_t)cap * sizeof(*g));
|
||||
if (!g) {
|
||||
return;
|
||||
}
|
||||
ctx->tf_releases = g;
|
||||
ctx->tf_release_cap = cap;
|
||||
}
|
||||
const char **files = NULL;
|
||||
int nfiles = 0;
|
||||
char *values = tf_release_values(t, body, &files, &nfiles);
|
||||
ctx->tf_releases[ctx->tf_release_n++] = (cbm_tf_release_t){src, chart, values};
|
||||
/* the values files it reads, like a kustomization's valuesFile: an edit of
|
||||
* one alone renders the release again (pipeline_incremental.c) */
|
||||
for (int i = 0; i < nfiles; i++) {
|
||||
const char *mqn = cbm_fqn_module(t->arena, ctx->project_name, files[i]);
|
||||
const cbm_gbuf_node_t *mod = mqn ? cbm_gbuf_find_by_qn(ctx->gbuf, mqn) : NULL;
|
||||
if (mod && mod->id != src) {
|
||||
cbm_gbuf_insert_edge(ctx->gbuf, src, mod->id, "IMPORTS", "{\"local_name\":\"values\"}");
|
||||
t->edges++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void tf_helm_release(tf_t *t, TSNode block, const char *name) {
|
||||
TSNode body = {0};
|
||||
for (uint32_t i = 0; i < ts_node_named_child_count(block); i++) {
|
||||
@@ -320,6 +839,7 @@ static void tf_helm_release(tf_t *t, TSNode block, const char *name) {
|
||||
external ? ",\"external\":true" : "", (int)ts_node_start_point(block).row + SKIP_ONE);
|
||||
cbm_gbuf_insert_edge(t->ctx->gbuf, src, tgt, "DEPLOYS", props);
|
||||
t->edges++;
|
||||
tf_release_push(t, src, tgt, body);
|
||||
}
|
||||
|
||||
/* The text of a string_lit label: "helm_release" -> helm_release. */
|
||||
|
||||
@@ -2466,8 +2466,9 @@ static bool closure_dir_has_compose(const char *repo_path, const char *rel, cons
|
||||
return found;
|
||||
}
|
||||
|
||||
/* The nearest directory at or above `rel`'s that holds a Chart.yaml, into out
|
||||
* ("" for the repository root); false when none does. */
|
||||
/* The nearest directory at or above `rel`'s that holds a Chart.yaml, or is a
|
||||
* werf `.helm` chart, into out ("" for the repository root); false when none
|
||||
* is. */
|
||||
static bool closure_chart_dir_of(const char *repo_path, const char *rel, char *out, size_t sz) {
|
||||
snprintf(out, sz, "%s", rel);
|
||||
char *cut = strrchr(out, '/');
|
||||
@@ -2485,6 +2486,9 @@ static bool closure_chart_dir_of(const char *repo_path, const char *rel, char *o
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (out[0] && cbm_werf_helm_dir(repo_path, out)) {
|
||||
return true;
|
||||
}
|
||||
if (!out[0]) {
|
||||
return false;
|
||||
}
|
||||
@@ -2492,6 +2496,33 @@ static bool closure_chart_dir_of(const char *repo_path, const char *rel, char *o
|
||||
}
|
||||
}
|
||||
|
||||
/* Is `rel` a file a release reads as values (an IMPORTS {local_name:
|
||||
* valuesFile | values} edge reaches its node)? */
|
||||
static bool closure_release_values(cbm_store_t *store, const char *project, const char *rel) {
|
||||
cbm_node_t *nodes = NULL;
|
||||
int count = 0;
|
||||
if (cbm_store_find_nodes_by_file(store, project, rel, &nodes, &count) != CBM_STORE_OK) {
|
||||
return false;
|
||||
}
|
||||
bool hit = false;
|
||||
for (int i = 0; i < count && !hit; i++) {
|
||||
cbm_edge_t *edges = NULL;
|
||||
int ne = 0;
|
||||
if (cbm_store_find_edges_by_target_type(store, nodes[i].id, "IMPORTS", &edges, &ne) !=
|
||||
CBM_STORE_OK) {
|
||||
continue;
|
||||
}
|
||||
for (int j = 0; j < ne && !hit; j++) {
|
||||
const char *p = edges[j].properties_json;
|
||||
hit = p && (strstr(p, "\"local_name\":\"valuesFile\"") ||
|
||||
strstr(p, "\"local_name\":\"values\""));
|
||||
}
|
||||
cbm_store_free_edges(edges, ne);
|
||||
}
|
||||
cbm_store_free_nodes(nodes, count);
|
||||
return hit;
|
||||
}
|
||||
|
||||
/* Kubernetes inputs reach past their own file (pass_k8s.c): a kustomization
|
||||
* decides which manifests deploy together and in which namespace, a
|
||||
* Chart.yaml links other charts, and a manifest's Services select workloads
|
||||
@@ -2508,7 +2539,9 @@ static const char *closure_k8s_decline(cbm_store_t *store, const char *project,
|
||||
if (cbm_is_kustomize_file(base) || strcmp(base, "Kustomization") == 0) {
|
||||
return "k8s_bundle_input_changed";
|
||||
}
|
||||
if (strcmp(base, "Chart.yaml") == 0 || strcmp(base, "Chart.yml") == 0) {
|
||||
/* A werf.yaml names the chart of the `.helm` dir beside it. */
|
||||
if (strcmp(base, "Chart.yaml") == 0 || strcmp(base, "Chart.yml") == 0 ||
|
||||
strcmp(base, "werf.yaml") == 0 || strcmp(base, "werf.yml") == 0) {
|
||||
return "helm_chart_changed";
|
||||
}
|
||||
/* A chart's values file CONFIGURES it and its templates/ are what it
|
||||
@@ -2528,6 +2561,12 @@ static const char *closure_k8s_decline(cbm_store_t *store, const char *project,
|
||||
if (strcmp(base, ".env") == 0 && closure_dir_has_compose(repo_path, rel, base)) {
|
||||
return "compose_env_changed";
|
||||
}
|
||||
/* A release's values file -- a kustomization's valuesFile, a Terraform
|
||||
* helm_release's file(...) -- feeds the images the release renders,
|
||||
* which a repair of the values file alone does not redraw. */
|
||||
if (closure_release_values(store, project, rel)) {
|
||||
return "helm_release_values_changed";
|
||||
}
|
||||
cbm_node_t *nodes = NULL;
|
||||
int count = 0;
|
||||
if (cbm_store_find_nodes_by_file(store, project, rel, &nodes, &count) != CBM_STORE_OK) {
|
||||
|
||||
@@ -407,6 +407,16 @@ typedef struct {
|
||||
int reader_count;
|
||||
} cbm_envjoin_base_t;
|
||||
|
||||
/* A Terraform helm_release (pass_terraform.c): the chart it DEPLOYS and its
|
||||
* values -- `values = [...]` coalesced in order, then `set` -- as JSON; NULL
|
||||
* when a part is not a literal (an interpolation, a variable). pass_k8s.c
|
||||
* renders the chart's images with them. */
|
||||
typedef struct cbm_tf_release {
|
||||
int64_t src; /* the resource.helm_release node */
|
||||
int64_t chart; /* the Chart node */
|
||||
char *values; /* owned (cbm_free); NULL: unknown */
|
||||
} cbm_tf_release_t;
|
||||
|
||||
/* Shared context passed to each pass function.
|
||||
* Derived from cbm_pipeline_t fields during run. */
|
||||
typedef struct {
|
||||
@@ -513,6 +523,12 @@ typedef struct {
|
||||
const struct cbm_doclink_scope *doc_link_base;
|
||||
int doc_link_base_count;
|
||||
bool doc_links_failed;
|
||||
|
||||
/* The run's Terraform helm_releases (pass_terraform.c fills them, the K8s
|
||||
* pass reads and frees them right after). */
|
||||
cbm_tf_release_t *tf_releases;
|
||||
int tf_release_n;
|
||||
int tf_release_cap;
|
||||
} cbm_pipeline_ctx_t;
|
||||
|
||||
/* Origin-aware LSP returns only success/failure. Never retain partial output
|
||||
@@ -1087,6 +1103,9 @@ bool cbm_is_k8s_manifest(const char *name, const char *content);
|
||||
/* A Helm values file name: values.yaml / .yml, values-<x>.yaml / .yml
|
||||
* (pass_k8s.c; beside Chart.yaml it CONFIGURES the chart). */
|
||||
bool cbm_helm_values_name(const char *base);
|
||||
/* Is `dir` (repository relative) a werf chart: a `.helm` directory without a
|
||||
* Chart.yaml whose parent holds werf.yaml (pass_k8s.c)? */
|
||||
bool cbm_werf_helm_dir(const char *repo_path, const char *dir);
|
||||
|
||||
/* Secret detection */
|
||||
bool cbm_is_secret_binding(const char *key, const char *value);
|
||||
@@ -1286,6 +1305,7 @@ typedef struct {
|
||||
const char *via; /* "skaffold" / "compose" */
|
||||
const char *image; /* the reference, variables expanded */
|
||||
const char *dockerfile; /* repository path */
|
||||
const char *build_args; /* compose build.args as a JSON object; NULL: none */
|
||||
int line;
|
||||
} cbm_build_site_t;
|
||||
typedef void (*cbm_build_site_fn)(const cbm_build_site_t *site, void *ud);
|
||||
@@ -1296,10 +1316,17 @@ typedef void (*cbm_build_site_fn)(const cbm_build_site_t *site, void *ud);
|
||||
* depends_on and links list, and the names of the environment variables its
|
||||
* environment sets (map keys / list items' names -- never a value), with
|
||||
* their 1-based lines and the service name each value addresses (a bare
|
||||
* name, `name:port`, a URL's host; NULL: none -- cbm_env_address). */
|
||||
* name, `name:port`, a URL's host; NULL: none -- cbm_env_address).
|
||||
* image_default_of: the variables whose inline default (${X:-d}) named the
|
||||
* image; image_override_of: those whose default the project's .env replaced
|
||||
* (JSON arrays, NULL: none), and default_image the image those defaults
|
||||
* name. */
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const char *image;
|
||||
const char *image_default_of;
|
||||
const char *image_override_of;
|
||||
const char *default_image;
|
||||
const char *dockerfile;
|
||||
const char **deps;
|
||||
int ndeps;
|
||||
@@ -1367,6 +1394,12 @@ bool cbm_repo_rel_join(const char *dir, const char *v, char *out, size_t sz);
|
||||
* for an official image; registry "unknown" when the reference names no
|
||||
* host -- none is invented), made when new; 0 for scratch or no image. */
|
||||
int64_t cbm_pipeline_image_node(cbm_pipeline_ctx_t *ctx, const char *ref);
|
||||
/* Does `ref` follow the image reference grammar (distribution/reference):
|
||||
* [host[:port]/]path[:tag][@algo:hex], path components lowercase alphanumerics
|
||||
* joined by `.`, `_`, `__` or dashes, a tag of word characters, `.` and `-`
|
||||
* (128 at most)? A value rendered from a template is checked before it names
|
||||
* an Image (pass_docker.c). */
|
||||
bool cbm_image_ref_valid(const char *ref);
|
||||
|
||||
/* A chart repository URL as an identity (pass_k8s.c): `url` trimmed, its
|
||||
* scheme and host lower-cased, trailing `/` and `.git` dropped. False for no
|
||||
@@ -1757,7 +1790,8 @@ int cbm_delta_drop_edges_from_label(cbm_store_t *store, const char *project, con
|
||||
const char *label);
|
||||
/* The edges that keep a file-less shared node alive. */
|
||||
#define SH_PROCESS_EDGES ((const char *const[]){"SPAWNS", NULL})
|
||||
#define DK_IMAGE_EDGES ((const char *const[]){"DEPENDS_ON", "USAGE", NULL})
|
||||
/* An Image a release DEPLOYS (pass_k8s.c release images) may have no other edge. */
|
||||
#define DK_IMAGE_EDGES ((const char *const[]){"DEPENDS_ON", "USAGE", "DEPLOYS", NULL})
|
||||
/* Pre-seed proxy nodes with their REAL database ids and move the gbuf id
|
||||
* watermark above MAX(id); returns that max id, or -1 on failure. */
|
||||
int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *gbuf);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
apiVersion: v2
|
||||
name: cx
|
||||
version: 0.1.0
|
||||
appVersion: "1.0"
|
||||
@@ -0,0 +1,4 @@
|
||||
{{- define "cx.image" -}}
|
||||
{{- $reg := .registry -}}
|
||||
{{- printf "%s/%s:%s" $reg .repository .tag -}}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: cx
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: main
|
||||
image: "{{ include "cx.image" .Values.image }}"
|
||||
{{- range .Values.sidecars }}
|
||||
- name: {{ .name }}
|
||||
image: "{{ .image.repository }}:{{ .image.tag }}"
|
||||
{{- end }}
|
||||
@@ -0,0 +1,12 @@
|
||||
common: &common
|
||||
pullPolicy: IfNotPresent
|
||||
image:
|
||||
<<: *common
|
||||
registry: ghcr.io
|
||||
repository: acme/cx
|
||||
tag: "1.0"
|
||||
sidecars:
|
||||
- name: a
|
||||
image: {repository: ghcr.io/acme/cx-side, tag: "2.0"}
|
||||
- name: b
|
||||
image: {repository: ghcr.io/acme/cx-side, tag: "2.1"}
|
||||
@@ -4445,6 +4445,7 @@ static char *gpf_chart_edges(const GpfFile *files, int nfiles) {
|
||||
"SELECT coalesce(group_concat(x, char(10)), '') FROM (SELECT CASE WHEN "
|
||||
"s.label='File' THEN s.file_path ELSE s.name END || ' -> ' || t.label || ':' || "
|
||||
"t.name || ' (' || e.type || coalesce(' ' || json_extract(e.properties,'$.ref'), '') "
|
||||
"|| coalesce(' ' || json_extract(e.properties,'$.phase'), '') "
|
||||
"|| ')' 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 ((s.label='Chart' AND e.type='DEPLOYS') OR "
|
||||
"(t.label='Chart' AND e.type='CONFIGURES')) ORDER BY x)";
|
||||
@@ -4462,8 +4463,8 @@ static char *gpf_chart_edges(const GpfFile *files, int nfiles) {
|
||||
|
||||
/* A chart deploys what its templates render: Chart -DEPLOYS {via: chart}->
|
||||
* each object of its templates/ (a subchart's objects are the subchart's),
|
||||
* and each Image those objects name literally ({ref: image}) -- a templated
|
||||
* image names none, and no values default is read. Its values files
|
||||
* and each Image those objects use ({ref: image}) -- a templated one rendered
|
||||
* from values.yaml (`nginx`: host-less, kept as written). Its values files
|
||||
* (values.yaml, values-*.yaml beside Chart.yaml) CONFIGURE it; a values file
|
||||
* elsewhere (ci/) does not. */
|
||||
TEST(probe_helm_chart_deploys_its_objects) {
|
||||
@@ -4492,6 +4493,7 @@ TEST(probe_helm_chart_deploys_its_objects) {
|
||||
"db -> Image:ghcr.io/acme/db (DEPLOYS image)\n"
|
||||
"db -> Resource:StatefulSet/db (DEPLOYS)\n"
|
||||
"web -> Image:ghcr.io/acme/sidecar (DEPLOYS image)\n"
|
||||
"web -> Image:nginx (DEPLOYS image)\n"
|
||||
"web -> Resource:Deployment/web (DEPLOYS)\n"
|
||||
"web -> Resource:Service/web (DEPLOYS)";
|
||||
if (!got || strcmp(got, want) != 0) {
|
||||
@@ -4503,6 +4505,793 @@ TEST(probe_helm_chart_deploys_its_objects) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A chart also deploys the CRDs of its crds/ ({phase: crds}), which Helm
|
||||
* installs before its templates. werf deploys the `.helm` dir beside its
|
||||
* werf.yaml as a chart without Chart.yaml, named after the werf project
|
||||
* (templated werf.yaml lines before it are skipped); a `.helm` with its own
|
||||
* Chart.yaml stays that chart, and a werf.yaml without `.helm` makes none. */
|
||||
TEST(probe_helm_chart_crds_and_werf_chart) {
|
||||
static const GpfFile files[] = {
|
||||
{"charts/op/Chart.yaml", "apiVersion: v2\nname: op\nversion: 1.0.0\n"},
|
||||
{"charts/op/crds/widget.yaml",
|
||||
"apiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\nmetadata:\n"
|
||||
" name: widgets.acme.io\nspec:\n group: acme.io\n names:\n kind: Widget\n"
|
||||
" plural: widgets\n scope: Namespaced\n"},
|
||||
{"charts/op/templates/deploy.yaml",
|
||||
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: op\n"},
|
||||
{"site/werf.yaml", "{{- $_ := set . \"Env\" (.Env | default \"prod\") }}\n\n"
|
||||
"project: site-web\nconfigVersion: 1\n---\nimage: web\nfrom: nginx\n"},
|
||||
{"site/.helm/values.yaml", "replicas: 1\n"},
|
||||
{"site/.helm/templates/deploy.yaml",
|
||||
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: site\nspec:\n"
|
||||
" replicas: {{ .Values.replicas }}\n"},
|
||||
{"plain/werf.yaml", "project: plain\nconfigVersion: 1\n"},
|
||||
{"own/werf.yaml", "project: own-werf\nconfigVersion: 1\n"},
|
||||
{"own/.helm/Chart.yaml", "apiVersion: v2\nname: own\nversion: 0.1.0\n"},
|
||||
{"own/.helm/templates/cm.yaml",
|
||||
"apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: own\n"}};
|
||||
char *got = gpf_chart_edges(files, 10);
|
||||
static const char want[] =
|
||||
"op -> Resource:CustomResourceDefinition/widgets.acme.io (DEPLOYS crds)\n"
|
||||
"op -> Resource:Deployment/op (DEPLOYS)\n"
|
||||
"own -> Resource:ConfigMap/own (DEPLOYS)\n"
|
||||
"site-web -> Resource:Deployment/site (DEPLOYS)\n"
|
||||
"site/.helm/values.yaml -> Chart:site-web (CONFIGURES)";
|
||||
if (!got || strcmp(got, want) != 0) {
|
||||
fprintf(stderr, "chart edges got:\n%s\nwant:\n%s\n", got ? got : "(null)", want);
|
||||
free(got);
|
||||
FAIL("crds/ and werf chart edge set");
|
||||
}
|
||||
free(got);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Every edge into an Image (or out of one into an Image), one line each:
|
||||
* `source -> Image:name TYPE image=.. [via=..] [ref=..] [default_of=..]
|
||||
* [override_of=..] [inferred=..] [values_key=..]`, sorted. */
|
||||
static char *gpf_image_edges(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(group_concat(x, char(10)), '') FROM (SELECT s.label || ':' || s.name "
|
||||
"|| ' -> Image:' || t.name || ' ' || e.type || ' image=' || "
|
||||
"coalesce(json_extract(e.properties,'$.image'),'-') || "
|
||||
"coalesce(' via=' || json_extract(e.properties,'$.via'), '') || "
|
||||
"coalesce(' ref=' || json_extract(e.properties,'$.ref'), '') || "
|
||||
"coalesce(' default_of=' || json_extract(e.properties,'$.default_of'), '') || "
|
||||
"coalesce(' override_of=' || json_extract(e.properties,'$.override_of'), '') || "
|
||||
"coalesce(' inferred=' || json_extract(e.properties,'$.inferred'), '') || "
|
||||
"coalesce(' values_key=' || json_extract(e.properties,'$.values_key'), '') 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 t.label='Image' ORDER BY x)";
|
||||
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;
|
||||
}
|
||||
|
||||
static bool gpf_expect_image_edges(const GpfFile *files, int n, const char *want) {
|
||||
char *got = gpf_image_edges(files, n);
|
||||
bool ok = got && strcmp(got, want) == 0;
|
||||
if (!ok) {
|
||||
fprintf(stderr, "image edges got:\n%s\nwant:\n%s\n", got ? got : "(null)", want);
|
||||
}
|
||||
free(got);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* An image a default value names keeps its Image, its edge marked
|
||||
* default_of with the ARGs / variables whose default supplied it (an ARG
|
||||
* chain names each); a literal FROM is unmarked. A build's arguments (compose
|
||||
* build.args, docker build --build-arg) make the Image it builds DEPEND_ON
|
||||
* the base they choose {override_of}; compose's .env replacing an inline
|
||||
* default is the effective edge's override_of, the default's image kept as
|
||||
* {ref: image_default} -- one the service does not run. */
|
||||
TEST(probe_docker_default_images_marked_and_overrides) {
|
||||
static const GpfFile files[] = {
|
||||
{"src/web/Dockerfile", "ARG GO=1.22\nARG BASE=golang:${GO}\nFROM ${BASE} AS build\n"
|
||||
"RUN go build -o /app .\nFROM alpine:3.20\n"
|
||||
"COPY --from=build /app /app\n"},
|
||||
{"src/api/Dockerfile", "ARG PY=3.12\nFROM python:${PY}-slim\n"},
|
||||
{".env", "REG=ghcr.io/acme\nAPI_IMG=ghcr.io/acme/api-custom\n"},
|
||||
{"compose.yaml", "services:\n web:\n image: ${REG}/web:${TAG:-dev}\n build:\n"
|
||||
" context: ./src/web\n args:\n GO: \"1.23\"\n"
|
||||
" api:\n image: ${API_IMG:-ghcr.io/acme/api}\n build: ./src/api\n"},
|
||||
{"scripts/build.sh", "#!/bin/sh\nIMAGE=${IMAGE:-ghcr.io/acme/api-ci}\n"
|
||||
"docker build --build-arg PY=3.13 -t \"$IMAGE:1\" src/api\n"}};
|
||||
static const char want[] =
|
||||
"Image:ghcr.io/acme/api-ci -> Image:python DEPENDS_ON image=python:3.13-slim via=from "
|
||||
"override_of=[\"PY\"]\n"
|
||||
"Image:ghcr.io/acme/web -> Image:golang DEPENDS_ON image=golang:1.23 via=from "
|
||||
"default_of=[\"BASE\"] override_of=[\"GO\"]\n"
|
||||
"Module:src/api/Dockerfile -> Image:python DEPENDS_ON image=python:3.12-slim via=from "
|
||||
"default_of=[\"PY\"]\n"
|
||||
"Module:src/web/Dockerfile -> Image:alpine DEPENDS_ON image=alpine:3.20 via=from\n"
|
||||
"Module:src/web/Dockerfile -> Image:golang DEPENDS_ON image=golang:1.22 via=from "
|
||||
"default_of=[\"GO\",\"BASE\"]\n"
|
||||
"Resource:Service/api -> Image:ghcr.io/acme/api USAGE image=ghcr.io/acme/api "
|
||||
"ref=image_default default_of=[\"API_IMG\"]\n"
|
||||
"Resource:Service/api -> Image:ghcr.io/acme/api-custom USAGE image=ghcr.io/acme/api-custom "
|
||||
"ref=image override_of=[\"API_IMG\"]\n"
|
||||
"Resource:Service/web -> Image:ghcr.io/acme/web USAGE image=ghcr.io/acme/web:dev "
|
||||
"ref=image default_of=[\"TAG\"]";
|
||||
if (!gpf_expect_image_edges(files, 5, want)) {
|
||||
FAIL("default-valued images and their in-repo overrides");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The chart a Helm template renders (helm_template.c): the container image
|
||||
* from the chart family's defaults -- an include helper (bitnami
|
||||
* common.images.image, a dict argument, .Chart.AppVersion), a `with`
|
||||
* binding, the taken branch of an if / else, a subchart under its alias with
|
||||
* the parent's values and global. The object USES it {default_of: the values
|
||||
* keys read}; the chart DEPLOYS it. A value that does not render -- a
|
||||
* .Release field, lookup, a missing define, a field of a missing map -- names
|
||||
* no Image. */
|
||||
TEST(probe_helm_template_images_render_with_defaults) {
|
||||
static const GpfFile files[] = {
|
||||
{"charts/app/Chart.yaml",
|
||||
"apiVersion: v2\nname: app\nversion: 1.2.3\nappVersion: \"2.0.1\"\ndependencies:\n"
|
||||
" - name: db\n version: 0.1.0\n alias: database\n"},
|
||||
{"charts/app/values.yaml",
|
||||
"image:\n registry: ghcr.io\n repository: acme/app\n tag: \"\"\n digest: \"\"\n"
|
||||
"sidecar:\n image: { repository: docker.io/library/busybox, tag: 1.36 }\nglobal: {}\n"
|
||||
"database:\n image:\n tag: \"17\"\n"},
|
||||
{"charts/app/templates/_helpers.tpl",
|
||||
"{{- define \"common.images.image\" -}}\n"
|
||||
"{{- $registryName := default .imageRoot.registry ((.global).imageRegistry) -}}\n"
|
||||
"{{- $termination := .imageRoot.tag | toString -}}\n"
|
||||
"{{- if not .imageRoot.tag }}\n {{- if .chart }}\n"
|
||||
" {{- $termination = .chart.AppVersion | toString -}}\n {{- end -}}\n{{- end -}}\n"
|
||||
"{{- if $registryName }}\n"
|
||||
"{{- printf \"%s/%s:%s\" $registryName .imageRoot.repository $termination -}}\n"
|
||||
"{{- else -}}\n{{- printf \"%s:%s\" .imageRoot.repository $termination -}}\n"
|
||||
"{{- end -}}\n{{- end -}}\n"},
|
||||
{"charts/app/templates/deploy.yaml",
|
||||
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: app\nspec:\n template:\n"
|
||||
" spec:\n containers:\n - name: app\n image: {{ include "
|
||||
"\"common.images.image\" (dict \"imageRoot\" .Values.image \"global\" .Values.global "
|
||||
"\"chart\" .Chart) }}\n - name: side\n {{- with .Values.sidecar.image }}\n"
|
||||
" image: \"{{ .repository }}:{{ .tag }}\"\n {{- end }}\n"
|
||||
" - name: rel\n image: \"ghcr.io/acme/{{ .Release.Name }}:1\"\n"
|
||||
" - name: miss\n image: {{ include \"nope\" . }}\n"
|
||||
" - name: deep\n image: \"{{ .Values.absent.repository }}\"\n"},
|
||||
{"charts/app/charts/db/Chart.yaml",
|
||||
"apiVersion: v2\nname: db\nversion: 0.1.0\nappVersion: \"16.4\"\n"},
|
||||
{"charts/app/charts/db/values.yaml",
|
||||
"image:\n repository: bitnami/postgresql\n tag: \"\"\n digest: \"\"\n"},
|
||||
{"charts/app/charts/db/templates/sts.yaml",
|
||||
"apiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: db\nspec:\n template:\n"
|
||||
" spec:\n containers:\n - name: db\n {{- if .Values.image.digest }}\n"
|
||||
" image: \"{{ .Values.global.imageRegistry | default \"docker.io\" }}/"
|
||||
"{{ .Values.image.repository }}@{{ .Values.image.digest }}\"\n {{- else }}\n"
|
||||
" image: \"{{ .Values.global.imageRegistry | default \"docker.io\" }}/"
|
||||
"{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}\"\n"
|
||||
" {{- end }}\n"}};
|
||||
static const char want[] =
|
||||
"Chart:app -> Image:docker.io/library/busybox DEPLOYS image=- via=chart ref=image "
|
||||
"default_of=[\"sidecar.image.repository\",\"sidecar.image.tag\"]\n"
|
||||
"Chart:app -> Image:ghcr.io/acme/app DEPLOYS image=- via=chart ref=image "
|
||||
"default_of=[\"image.registry\",\"global.imageRegistry\",\"image.tag\","
|
||||
"\"image.repository\"]\n"
|
||||
"Chart:db -> Image:docker.io/bitnami/postgresql DEPLOYS image=- via=chart ref=image "
|
||||
"default_of=[\"global.imageRegistry\",\"image.repository\",\"image.tag\"]\n"
|
||||
"Resource:Deployment/app -> Image:docker.io/library/busybox USAGE "
|
||||
"image=docker.io/library/busybox:1.36 ref=image "
|
||||
"default_of=[\"sidecar.image.repository\",\"sidecar.image.tag\"]\n"
|
||||
"Resource:Deployment/app -> Image:ghcr.io/acme/app USAGE image=ghcr.io/acme/app:2.0.1 "
|
||||
"ref=image default_of=[\"image.registry\",\"global.imageRegistry\",\"image.tag\","
|
||||
"\"image.repository\"]\n"
|
||||
"Resource:StatefulSet/db -> Image:docker.io/bitnami/postgresql USAGE "
|
||||
"image=docker.io/bitnami/postgresql:17 ref=image "
|
||||
"default_of=[\"global.imageRegistry\",\"image.repository\",\"image.tag\"]";
|
||||
if (!gpf_expect_image_edges(files, 7, want)) {
|
||||
FAIL("Helm template images rendered with the chart family's defaults");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A release renders its chart with its values over the defaults: of a chart
|
||||
* in the repository each image that differs is the release's own {via,
|
||||
* override_of} -- a Flux HelmRelease's spec.values (a Git source), a
|
||||
* kustomize helmCharts entry's valuesFile + valuesInline (chartHome charts/),
|
||||
* a Terraform helm_release's values + set. Of an external chart the values
|
||||
* name images by convention -- `repository` (+ tag / registry) naming a
|
||||
* registry host -- {inferred: values}; a host-less repository names none. */
|
||||
TEST(probe_helm_release_images) {
|
||||
static const GpfFile files[] = {
|
||||
{"charts/app/Chart.yaml", "apiVersion: v2\nname: app\nversion: 1.0.0\n"},
|
||||
{"charts/app/values.yaml", "image:\n repository: ghcr.io/acme/app\n tag: \"1.0\"\n"},
|
||||
{"charts/app/templates/deploy.yaml",
|
||||
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: app\nspec:\n template:\n"
|
||||
" spec:\n containers:\n - name: app\n"
|
||||
" image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"},
|
||||
{"flux/app.yaml",
|
||||
"apiVersion: helm.toolkit.fluxcd.io/v2\nkind: HelmRelease\nmetadata:\n name: app\nspec:\n"
|
||||
" chart:\n spec:\n chart: ./charts/app\n sourceRef:\n"
|
||||
" kind: GitRepository\n name: repo\n values:\n image:\n"
|
||||
" tag: \"9.9\"\n---\n"
|
||||
"apiVersion: helm.toolkit.fluxcd.io/v2\nkind: HelmRelease\nmetadata:\n name: sonarr\n"
|
||||
"spec:\n chart:\n spec:\n chart: app-template\n sourceRef:\n"
|
||||
" kind: HelmRepository\n name: bjw-s\n values:\n controllers:\n"
|
||||
" main:\n containers:\n main:\n image:\n"
|
||||
" repository: ghcr.io/home-operations/sonarr\n tag: 4.0.15\n"
|
||||
" side:\n image:\n repository: busybox\n"
|
||||
" tag: \"1.36\"\n"},
|
||||
{"kz/kustomization.yaml",
|
||||
"apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nhelmCharts:\n"
|
||||
" - name: app\n releaseName: kapp\n valuesFile: values-prod.yaml\n"
|
||||
" valuesInline:\n image:\n tag: \"5.5\"\n"},
|
||||
{"kz/values-prod.yaml", "image:\n repository: quay.io/acme/app\n tag: \"4.4\"\n"},
|
||||
{"kz/charts/app/Chart.yaml", "apiVersion: v2\nname: app\nversion: 1.0.0\n"},
|
||||
{"kz/charts/app/values.yaml", "image:\n repository: ghcr.io/acme/app\n tag: \"1.0\"\n"},
|
||||
{"kz/charts/app/templates/deploy.yaml",
|
||||
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: kapp\nspec:\n template:\n"
|
||||
" spec:\n containers:\n - name: app\n"
|
||||
" image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"},
|
||||
{"tf/main.tf", "resource \"helm_release\" \"app\" {\n name = \"app\"\n"
|
||||
" chart = \"../charts/app\"\n set {\n name = \"image.tag\"\n"
|
||||
" value = \"7.7\"\n }\n}\n"}};
|
||||
static const char want[] =
|
||||
"Chart:app -> Image:ghcr.io/acme/app DEPLOYS image=- via=chart ref=image "
|
||||
"default_of=[\"image.repository\",\"image.tag\"]\n"
|
||||
"Chart:app -> Image:ghcr.io/acme/app DEPLOYS image=- via=chart ref=image "
|
||||
"default_of=[\"image.repository\",\"image.tag\"]\n"
|
||||
"Class:resource.helm_release.app -> Image:ghcr.io/acme/app DEPLOYS "
|
||||
"image=ghcr.io/acme/app:7.7 via=terraform ref=image override_of=[\"image.tag\"]\n"
|
||||
"Resource:Deployment/app -> Image:ghcr.io/acme/app USAGE image=ghcr.io/acme/app:1.0 "
|
||||
"ref=image default_of=[\"image.repository\",\"image.tag\"]\n"
|
||||
"Resource:Deployment/kapp -> Image:ghcr.io/acme/app USAGE image=ghcr.io/acme/app:1.0 "
|
||||
"ref=image default_of=[\"image.repository\",\"image.tag\"]\n"
|
||||
"Resource:HelmRelease/app -> Image:ghcr.io/acme/app DEPLOYS image=ghcr.io/acme/app:9.9 "
|
||||
"via=helmrelease ref=image override_of=[\"image.tag\"]\n"
|
||||
"Resource:HelmRelease/sonarr -> Image:ghcr.io/home-operations/sonarr DEPLOYS "
|
||||
"image=ghcr.io/home-operations/sonarr:4.0.15 via=helmrelease ref=image inferred=values "
|
||||
"values_key=controllers.main.containers.main.image\n"
|
||||
"Resource:Kustomization -> Image:quay.io/acme/app DEPLOYS image=quay.io/acme/app:5.5 "
|
||||
"via=helmcharts ref=image override_of=[\"image.repository\",\"image.tag\"]";
|
||||
if (!gpf_expect_image_edges(files, 10, want)) {
|
||||
FAIL("release images: local renders and the external values convention");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The chart every release test below deploys: an image from two values. */
|
||||
#define GPF_APP_CHART \
|
||||
{"charts/app/Chart.yaml", "apiVersion: v2\nname: app\nversion: 1.0.0\n"}, \
|
||||
{"charts/app/values.yaml", "image:\n repository: ghcr.io/acme/app\n tag: \"1.0\"\n"}, \
|
||||
{ \
|
||||
"charts/app/templates/deploy.yaml", \
|
||||
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: app\nspec:\n template:\n" \
|
||||
" spec:\n containers:\n - name: app\n" \
|
||||
" image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n" \
|
||||
}
|
||||
|
||||
/* The edges of the chart's defaults (every test below has them). */
|
||||
#define GPF_APP_DEFAULTS \
|
||||
"Chart:app -> Image:ghcr.io/acme/app DEPLOYS image=- via=chart ref=image " \
|
||||
"default_of=[\"image.repository\",\"image.tag\"]\n" \
|
||||
"Resource:Deployment/app -> Image:ghcr.io/acme/app USAGE image=ghcr.io/acme/app:1.0 " \
|
||||
"ref=image default_of=[\"image.repository\",\"image.tag\"]"
|
||||
|
||||
/* A Terraform number set on a helm_release reaches the chart as the string
|
||||
* Terraform makes of it -- its exact decimal (1.1, 1.10 -> "1.1", 2e3 ->
|
||||
* "2000"), never a float's %.17g ("1.1000000000000001"). */
|
||||
TEST(probe_helm_release_terraform_number_set) {
|
||||
static const GpfFile files[] = {
|
||||
GPF_APP_CHART,
|
||||
{"tf/main.tf", "resource \"helm_release\" \"app\" {\n name = \"app\"\n"
|
||||
" chart = \"../charts/app\"\n set {\n name = \"image.tag\"\n"
|
||||
" value = 1.10\n }\n}\n"}};
|
||||
static const char want[] =
|
||||
"Chart:app -> Image:ghcr.io/acme/app DEPLOYS image=- via=chart ref=image "
|
||||
"default_of=[\"image.repository\",\"image.tag\"]\n"
|
||||
"Class:resource.helm_release.app -> Image:ghcr.io/acme/app DEPLOYS "
|
||||
"image=ghcr.io/acme/app:1.1 via=terraform ref=image override_of=[\"image.tag\"]\n"
|
||||
"Resource:Deployment/app -> Image:ghcr.io/acme/app USAGE image=ghcr.io/acme/app:1.0 "
|
||||
"ref=image default_of=[\"image.repository\",\"image.tag\"]";
|
||||
if (!gpf_expect_image_edges(files, 4, want)) {
|
||||
FAIL("a Terraform number set is its exact decimal");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A Flux HelmRelease whose values also come from a source not read --
|
||||
* valuesFrom, the chart's valuesFiles, a postRenderer that may patch the
|
||||
* images -- renders no image of its own: its values are not known whole. */
|
||||
TEST(probe_helm_release_flux_unread_sources) {
|
||||
static const GpfFile files[] = {
|
||||
GPF_APP_CHART,
|
||||
{"flux/app.yaml",
|
||||
"apiVersion: helm.toolkit.fluxcd.io/v2\nkind: HelmRelease\nmetadata:\n name: files\n"
|
||||
"spec:\n chart:\n spec:\n chart: ./charts/app\n valuesFiles:\n"
|
||||
" - charts/app/values-prod.yaml\n sourceRef:\n kind: GitRepository\n"
|
||||
" name: repo\n values:\n image:\n tag: \"9.9\"\n---\n"
|
||||
"apiVersion: helm.toolkit.fluxcd.io/v2\nkind: HelmRelease\nmetadata:\n name: from\n"
|
||||
"spec:\n chart:\n spec:\n chart: ./charts/app\n sourceRef:\n"
|
||||
" kind: GitRepository\n name: repo\n valuesFrom:\n"
|
||||
" - kind: ConfigMap\n name: app-values\n values:\n image:\n"
|
||||
" tag: \"8.8\"\n---\n"
|
||||
"apiVersion: helm.toolkit.fluxcd.io/v2\nkind: HelmRelease\nmetadata:\n name: post\n"
|
||||
"spec:\n chart:\n spec:\n chart: ./charts/app\n sourceRef:\n"
|
||||
" kind: GitRepository\n name: repo\n postRenderers:\n"
|
||||
" - kustomize:\n images:\n - name: ghcr.io/acme/app\n"
|
||||
" newName: quay.io/acme/app\n values:\n image:\n tag: \"7.7\"\n"}};
|
||||
if (!gpf_expect_image_edges(files, 4, GPF_APP_DEFAULTS)) {
|
||||
FAIL("a HelmRelease with a source not read has no image of its own");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A kustomize helmCharts entry with additionalValuesFiles, or a valuesMerge
|
||||
* other than the default override, renders no image of its own. */
|
||||
TEST(probe_helm_release_kustomize_unread_sources) {
|
||||
static const GpfFile files[] = {
|
||||
{"kz/kustomization.yaml",
|
||||
"apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nhelmCharts:\n"
|
||||
" - name: app\n releaseName: more\n additionalValuesFiles:\n"
|
||||
" - extra.yaml\n valuesInline:\n image:\n tag: \"5.5\"\n"
|
||||
" - name: app\n releaseName: merged\n valuesMerge: merge\n"
|
||||
" valuesInline:\n image:\n tag: \"6.6\"\n"},
|
||||
{"kz/charts/app/Chart.yaml", "apiVersion: v2\nname: app\nversion: 1.0.0\n"},
|
||||
{"kz/charts/app/values.yaml", "image:\n repository: ghcr.io/acme/app\n tag: \"1.0\"\n"},
|
||||
{"kz/charts/app/templates/deploy.yaml",
|
||||
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: app\nspec:\n template:\n"
|
||||
" spec:\n containers:\n - name: app\n"
|
||||
" image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"}};
|
||||
if (!gpf_expect_image_edges(files, 4, GPF_APP_DEFAULTS)) {
|
||||
FAIL("a helmCharts entry with a part not read has no image of its own");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A Terraform helm_release with a `dynamic "set"` block (entries made by a
|
||||
* for_each) renders no image of its own, whatever its literal sets say. */
|
||||
TEST(probe_helm_release_terraform_dynamic_set) {
|
||||
static const GpfFile files[] = {
|
||||
GPF_APP_CHART,
|
||||
{"tf/main.tf", "resource \"helm_release\" \"app\" {\n name = \"app\"\n"
|
||||
" chart = \"../charts/app\"\n dynamic \"set\" {\n"
|
||||
" for_each = var.sets\n content {\n name = set.key\n"
|
||||
" value = set.value\n }\n }\n set {\n name = \"image.tag\"\n"
|
||||
" value = \"3.3\"\n }\n}\n"}};
|
||||
if (!gpf_expect_image_edges(files, 4, GPF_APP_DEFAULTS)) {
|
||||
FAIL("a helm_release with a dynamic set has no image of its own");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A set_sensitive's value is never read: any set_sensitive leaves the
|
||||
* release without an image of its own, whatever its name -- image.tag or
|
||||
* db.password (which kept its edge in round 3). A postrender may rewrite
|
||||
* what a release deploys: no image of its own either. */
|
||||
TEST(probe_helm_release_terraform_sensitive_and_postrender) {
|
||||
static const GpfFile files[] = {
|
||||
GPF_APP_CHART,
|
||||
{"tf/main.tf",
|
||||
"resource \"helm_release\" \"tagged\" {\n name = \"tagged\"\n"
|
||||
" chart = \"../charts/app\"\n set {\n name = \"image.repository\"\n"
|
||||
" value = \"quay.io/acme/app\"\n }\n set_sensitive {\n name = \"image.tag\"\n"
|
||||
" value = var.tag\n }\n}\n"
|
||||
"resource \"helm_release\" \"pw\" {\n name = \"pw\"\n"
|
||||
" chart = \"../charts/app\"\n set {\n name = \"image.repository\"\n"
|
||||
" value = \"quay.io/acme/pw\"\n }\n set_sensitive {\n name = \"db.password\"\n"
|
||||
" value = var.pw\n }\n}\n"
|
||||
"resource \"helm_release\" \"post\" {\n name = \"post\"\n"
|
||||
" chart = \"../charts/app\"\n set {\n name = \"image.tag\"\n"
|
||||
" value = \"3.3\"\n }\n postrender {\n binary_path = \"./kustomize.sh\"\n }\n}\n"}};
|
||||
if (!gpf_expect_image_edges(files, 4, GPF_APP_DEFAULTS)) {
|
||||
FAIL("any set_sensitive and a postrender make release images unknown");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A set_sensitive whose name an image reads on another line ($tag, before
|
||||
* the image line): the release still has no image of its own -- never one
|
||||
* with the chart's default tag. */
|
||||
TEST(probe_helm_release_terraform_sensitive_read_earlier) {
|
||||
static const GpfFile files[] = {
|
||||
{"charts/web/Chart.yaml", "apiVersion: v2\nname: web\nversion: 1.0.0\nappVersion: \"1.0\"\n"},
|
||||
{"charts/web/values.yaml", "image:\n repository: ghcr.io/acme/web\n"},
|
||||
{"charts/web/templates/deploy.yaml",
|
||||
"{{- $tag := .Values.image.tag | default .Chart.AppVersion }}\n"
|
||||
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: web\nspec:\n template:\n"
|
||||
" spec:\n containers:\n - name: web\n"
|
||||
" image: \"{{ .Values.image.repository }}:{{ $tag }}\"\n"},
|
||||
{"tf/main.tf",
|
||||
"resource \"helm_release\" \"web\" {\n name = \"web\"\n"
|
||||
" chart = \"../charts/web\"\n set {\n name = \"image.repository\"\n"
|
||||
" value = \"quay.io/acme/web\"\n }\n set_sensitive {\n name = \"image.tag\"\n"
|
||||
" value = var.tag\n }\n}\n"}};
|
||||
static const char want[] =
|
||||
"Chart:web -> Image:ghcr.io/acme/web DEPLOYS image=- via=chart ref=image "
|
||||
"default_of=[\"image.repository\"]\n"
|
||||
"Resource:Deployment/web -> Image:ghcr.io/acme/web USAGE image=ghcr.io/acme/web:1.0 "
|
||||
"ref=image default_of=[\"image.repository\"]";
|
||||
if (!gpf_expect_image_edges(files, 4, want)) {
|
||||
FAIL("a set_sensitive read on another line makes the release images unknown");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A postrender as a dynamic block or as an attribute (provider v3) may
|
||||
* rewrite what a release deploys, as a block does: no image of its own. */
|
||||
TEST(probe_helm_release_terraform_postrender_forms) {
|
||||
static const GpfFile files[] = {
|
||||
GPF_APP_CHART,
|
||||
{"tf/main.tf",
|
||||
"resource \"helm_release\" \"dyn\" {\n name = \"dyn\"\n"
|
||||
" chart = \"../charts/app\"\n set {\n name = \"image.tag\"\n"
|
||||
" value = \"3.3\"\n }\n dynamic \"postrender\" {\n for_each = var.post\n"
|
||||
" content {\n binary_path = postrender.value\n }\n }\n}\n"
|
||||
"resource \"helm_release\" \"attr\" {\n name = \"attr\"\n"
|
||||
" chart = \"../charts/app\"\n set = [{ name = \"image.tag\", value = \"4.4\" }]\n"
|
||||
" postrender = { binary_path = \"./kustomize.sh\" }\n}\n"}};
|
||||
if (!gpf_expect_image_edges(files, 4, GPF_APP_DEFAULTS)) {
|
||||
FAIL("a dynamic or attribute postrender makes release images unknown");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A subchart included under an alias (redis as `cache`) has .Template.BasePath
|
||||
* app/charts/cache/templates, as Helm sets it (an image here shows it). Its
|
||||
* checksum of cm.yaml names its own file, which changes no map -- not app's
|
||||
* cm.yaml, which sets a value: its image stays known, as does app's (which
|
||||
* checksums its Secret).
|
||||
*
|
||||
* NOT a cross-file guarantee: app's cm.yaml sets .Values.image.tag, and Helm
|
||||
* renders a chart's files with one shared .Values (a subchart's is the
|
||||
* parent's .Values.<alias>). Each file renders alone here, so app's image is
|
||||
* known only because nothing models that set reaching deploy.yaml -- the
|
||||
* known limit (review round 4, R4-5), whose exact fix is to run the family's
|
||||
* files in Helm's order with one .Values (the run-included-files follow-up). */
|
||||
TEST(probe_helm_checksum_include_by_base_path) {
|
||||
static const GpfFile files[] = {
|
||||
{"charts/app/Chart.yaml", "apiVersion: v2\nname: app\nversion: 1.0.0\ndependencies:\n"
|
||||
" - name: redis\n version: 1.0.0\n alias: cache\n"},
|
||||
{"charts/app/values.yaml", "image:\n repository: ghcr.io/acme/app\n tag: \"1.0\"\n"},
|
||||
{"charts/app/templates/cm.yaml", "{{- $_ := set .Values.image \"tag\" \"2.0\" }}\n"
|
||||
"apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: app\n"},
|
||||
{"charts/app/templates/secret.yaml", "apiVersion: v1\nkind: Secret\nmetadata:\n name: app\n"},
|
||||
{"charts/app/templates/deploy.yaml",
|
||||
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: app\nspec:\n template:\n"
|
||||
" metadata:\n annotations:\n checksum/secret: "
|
||||
"{{ include (print $.Template.BasePath \"/secret.yaml\") . | sha256sum }}\n"
|
||||
" spec:\n containers:\n - name: app\n"
|
||||
" image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"},
|
||||
{"charts/app/charts/redis/Chart.yaml", "apiVersion: v2\nname: redis\nversion: 1.0.0\n"},
|
||||
{"charts/app/charts/redis/values.yaml",
|
||||
"image:\n repository: ghcr.io/acme/redis\n tag: \"7.0\"\n"},
|
||||
{"charts/app/charts/redis/templates/cm.yaml",
|
||||
"apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: redis\n"},
|
||||
{"charts/app/charts/redis/templates/sts.yaml",
|
||||
"apiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: redis\nspec:\n template:\n"
|
||||
" metadata:\n annotations:\n checksum/config: "
|
||||
"{{ include (print $.Template.BasePath \"/cm.yaml\") . | sha256sum }}\n"
|
||||
" spec:\n containers:\n - name: redis\n"
|
||||
" image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"
|
||||
" - name: path\n image: \"ghcr.io/{{ .Template.BasePath }}:1\"\n"}};
|
||||
static const char want[] =
|
||||
"Chart:app -> Image:ghcr.io/acme/app DEPLOYS image=- via=chart ref=image "
|
||||
"default_of=[\"image.repository\",\"image.tag\"]\n"
|
||||
"Chart:redis -> Image:ghcr.io/acme/redis DEPLOYS image=- via=chart ref=image "
|
||||
"default_of=[\"image.repository\",\"image.tag\"]\n"
|
||||
"Chart:redis -> Image:ghcr.io/app/charts/cache/templates DEPLOYS image=- via=chart "
|
||||
"ref=image\n"
|
||||
"Resource:Deployment/app -> Image:ghcr.io/acme/app USAGE image=ghcr.io/acme/app:1.0 "
|
||||
"ref=image default_of=[\"image.repository\",\"image.tag\"]\n"
|
||||
"Resource:StatefulSet/redis -> Image:ghcr.io/acme/redis USAGE "
|
||||
"image=ghcr.io/acme/redis:7.0 ref=image default_of=[\"image.repository\",\"image.tag\"]\n"
|
||||
"Resource:StatefulSet/redis -> Image:ghcr.io/app/charts/cache/templates USAGE "
|
||||
"image=ghcr.io/app/charts/cache/templates:1 ref=image";
|
||||
if (!gpf_expect_image_edges(files, 9, want)) {
|
||||
FAIL("a checksum include by .Template.BasePath names the chart's own file");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A kustomization that also transforms what it inflates (images:, patches)
|
||||
* has no release image of its own: the transformer may change it. */
|
||||
TEST(probe_helm_release_kustomize_transformers) {
|
||||
static const GpfFile files[] = {
|
||||
{"kz/kustomization.yaml",
|
||||
"apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nhelmCharts:\n"
|
||||
" - name: app\n releaseName: kapp\n valuesInline:\n image:\n"
|
||||
" tag: \"1.5\"\nimages:\n - name: ghcr.io/acme/app\n newTag: \"2.0\"\n"},
|
||||
{"kz/charts/app/Chart.yaml", "apiVersion: v2\nname: app\nversion: 1.0.0\n"},
|
||||
{"kz/charts/app/values.yaml", "image:\n repository: ghcr.io/acme/app\n tag: \"1.0\"\n"},
|
||||
{"kz/charts/app/templates/deploy.yaml",
|
||||
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: app\nspec:\n template:\n"
|
||||
" spec:\n containers:\n - name: app\n"
|
||||
" image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"}};
|
||||
if (!gpf_expect_image_edges(files, 4, GPF_APP_DEFAULTS)) {
|
||||
FAIL("a kustomization's own transformers make its release images unknown");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* An empty values.yaml is values {} (Helm's), not values not read: the
|
||||
* subchart under it still renders its image. */
|
||||
TEST(probe_helm_empty_values_file_renders) {
|
||||
static const GpfFile files[] = {
|
||||
{"charts/app/Chart.yaml",
|
||||
"apiVersion: v2\nname: app\nversion: 1.0.0\nappVersion: \"1.0\"\n"},
|
||||
{"charts/app/values.yaml", ""},
|
||||
{"charts/app/templates/deploy.yaml",
|
||||
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: app\nspec:\n template:\n"
|
||||
" spec:\n containers:\n - name: app\n"
|
||||
" image: \"ghcr.io/acme/{{ .Chart.Name }}:{{ .Chart.AppVersion }}\"\n"}};
|
||||
static const char want[] =
|
||||
"Chart:app -> Image:ghcr.io/acme/app DEPLOYS image=- via=chart ref=image\n"
|
||||
"Resource:Deployment/app -> Image:ghcr.io/acme/app USAGE image=ghcr.io/acme/app:1.0 "
|
||||
"ref=image";
|
||||
if (!gpf_expect_image_edges(files, 3, want)) {
|
||||
FAIL("an empty values.yaml renders with {}");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A subchart a dependency aliases renders with .Chart.Name its alias (Helm's
|
||||
* getAliasDependency), so an image naming the chart names the alias. */
|
||||
TEST(probe_helm_aliased_subchart_chart_name) {
|
||||
static const GpfFile files[] = {
|
||||
{"charts/app/Chart.yaml",
|
||||
"apiVersion: v2\nname: app\nversion: 1.0.0\ndependencies:\n - name: worker\n"
|
||||
" version: 0.1.0\n alias: worker-high\n"},
|
||||
{"charts/app/values.yaml", "worker-high: {}\n"},
|
||||
{"charts/app/charts/worker/Chart.yaml",
|
||||
"apiVersion: v2\nname: worker\nversion: 0.1.0\nappVersion: \"1.0\"\n"},
|
||||
{"charts/app/charts/worker/values.yaml", "{}\n"},
|
||||
{"charts/app/charts/worker/templates/deploy.yaml",
|
||||
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: worker\nspec:\n template:\n"
|
||||
" spec:\n containers:\n - name: worker\n"
|
||||
" image: \"ghcr.io/acme/{{ .Chart.Name }}:{{ .Chart.AppVersion }}\"\n"}};
|
||||
static const char want[] =
|
||||
"Chart:worker -> Image:ghcr.io/acme/worker-high DEPLOYS image=- via=chart ref=image\n"
|
||||
"Resource:Deployment/worker -> Image:ghcr.io/acme/worker-high USAGE "
|
||||
"image=ghcr.io/acme/worker-high:1.0 ref=image";
|
||||
if (!gpf_expect_image_edges(files, 5, want)) {
|
||||
FAIL("an aliased subchart's .Chart.Name is the alias");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* An image read from a values list keeps its mark on the chart's edge:
|
||||
* default_of names the list paths (images[0].repository) whole. */
|
||||
TEST(probe_helm_list_path_mark_on_chart_edge) {
|
||||
static const GpfFile files[] = {
|
||||
{"charts/app/Chart.yaml", "apiVersion: v2\nname: app\nversion: 1.0.0\n"},
|
||||
{"charts/app/values.yaml",
|
||||
"images:\n - repository: ghcr.io/acme/app\n tag: \"1.0\"\n"},
|
||||
{"charts/app/templates/deploy.yaml",
|
||||
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: app\nspec:\n template:\n"
|
||||
" spec:\n containers:\n - name: app\n"
|
||||
" image: \"{{ (index .Values.images 0).repository }}:"
|
||||
"{{ (index .Values.images 0).tag }}\"\n"}};
|
||||
static const char want[] =
|
||||
"Chart:app -> Image:ghcr.io/acme/app DEPLOYS image=- via=chart ref=image "
|
||||
"default_of=[\"images\",\"images[0].repository\",\"images[0].tag\"]\n"
|
||||
"Resource:Deployment/app -> Image:ghcr.io/acme/app USAGE image=ghcr.io/acme/app:1.0 "
|
||||
"ref=image default_of=[\"images\",\"images[0].repository\",\"images[0].tag\"]";
|
||||
if (!gpf_expect_image_edges(files, 3, want)) {
|
||||
FAIL("a list path's mark is kept whole on the chart edge");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
/* gpf_image_edges with symlinks laid in after the files: links[i] names a
|
||||
* repository path and the absolute or relative target it points at (POSIX
|
||||
* symlinks: the tests using it skip on Windows). */
|
||||
static char *gpf_image_edges_linked(const GpfFile *files, int nfiles, const GpfFile *links,
|
||||
int nlinks) {
|
||||
GpfProj lp;
|
||||
memset(&lp, 0, sizeof(lp));
|
||||
snprintf(lp.tmpdir, sizeof(lp.tmpdir), "/tmp/cbm_gpf_XXXXXX");
|
||||
if (!cbm_mkdtemp(lp.tmpdir)) {
|
||||
return NULL;
|
||||
}
|
||||
gpf_to_fwd_slashes(lp.tmpdir);
|
||||
for (int i = 0; i < nfiles + nlinks; i++) {
|
||||
const GpfFile *f = i < nfiles ? &files[i] : &links[i - nfiles];
|
||||
char path[700];
|
||||
snprintf(path, sizeof(path), "%s/%s", lp.tmpdir, f->name);
|
||||
char *slash = strrchr(path, '/');
|
||||
if (slash && slash > path + strlen(lp.tmpdir)) {
|
||||
*slash = '\0';
|
||||
cbm_mkdir_p(path, 0755);
|
||||
*slash = '/';
|
||||
}
|
||||
if (i >= nfiles) {
|
||||
if (symlink(f->content, path) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
FILE *fp = fopen(path, "wb");
|
||||
if (!fp) {
|
||||
return NULL;
|
||||
}
|
||||
fputs(f->content, fp);
|
||||
fclose(fp);
|
||||
}
|
||||
cbm_store_t *store = gpf_open_indexed(&lp);
|
||||
char *out = NULL;
|
||||
if (store) {
|
||||
sqlite3_stmt *st = NULL;
|
||||
const char *sql =
|
||||
"SELECT coalesce(group_concat(x, char(10)), '') FROM (SELECT s.label || ':' || s.name "
|
||||
"|| ' -> Image:' || t.name || ' ' || e.type || ' image=' || "
|
||||
"coalesce(json_extract(e.properties,'$.image'),'-') || "
|
||||
"coalesce(' via=' || json_extract(e.properties,'$.via'), '') 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 t.label='Image' ORDER BY x)";
|
||||
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;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* A values file a chart, a kustomization or a Terraform file() names by path
|
||||
* is read only when it is a file of the repository: a symlink out of it --
|
||||
* to a file holding an image -- is never followed. The chart whose values
|
||||
* file is such a link renders nothing (its values are unknown, not empty);
|
||||
* the releases reading one have no image of their own. */
|
||||
TEST(probe_helm_values_symlink_not_followed) {
|
||||
#ifdef _WIN32
|
||||
SKIP_PLATFORM("POSIX symlink contract");
|
||||
#else
|
||||
char outside[256];
|
||||
snprintf(outside, sizeof(outside), "/tmp/cbm_gpf_out_XXXXXX");
|
||||
ASSERT_NOT_NULL(cbm_mkdtemp(outside));
|
||||
char leak[300];
|
||||
snprintf(leak, sizeof(leak), "%s/values.yaml", outside);
|
||||
FILE *lf = fopen(leak, "wb");
|
||||
ASSERT_NOT_NULL(lf);
|
||||
fputs("image:\n repository: ghcr.io/evil/leak\n tag: \"6.6\"\n", lf);
|
||||
fclose(lf);
|
||||
const GpfFile files[] = {
|
||||
GPF_APP_CHART,
|
||||
{"linked/Chart.yaml", "apiVersion: v2\nname: linked\nversion: 1.0.0\n"},
|
||||
{"linked/templates/deploy.yaml",
|
||||
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: linked\nspec:\n template:\n"
|
||||
" spec:\n containers:\n - name: linked\n"
|
||||
" image: \"ghcr.io/acme/linked:{{ .Values.tag | default \"latest\" }}\"\n"},
|
||||
{"kz/kustomization.yaml",
|
||||
"apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nhelmCharts:\n"
|
||||
" - name: app\n releaseName: kapp\n valuesFile: prod.yaml\n"},
|
||||
{"kz/charts/app/Chart.yaml", "apiVersion: v2\nname: app\nversion: 1.0.0\n"},
|
||||
{"kz/charts/app/values.yaml", "image:\n repository: ghcr.io/acme/app\n tag: \"1.0\"\n"},
|
||||
{"kz/charts/app/templates/deploy.yaml",
|
||||
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: kapp\nspec:\n template:\n"
|
||||
" spec:\n containers:\n - name: app\n"
|
||||
" image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"}};
|
||||
const GpfFile links[] = {{"linked/values.yaml", leak}, {"kz/prod.yaml", leak}};
|
||||
char *got = gpf_image_edges_linked(files, 9, links, 2);
|
||||
th_rmtree(outside);
|
||||
static const char want[] =
|
||||
"Chart:app -> Image:ghcr.io/acme/app DEPLOYS image=- via=chart\n"
|
||||
"Chart:app -> Image:ghcr.io/acme/app DEPLOYS image=- via=chart\n"
|
||||
"Resource:Deployment/app -> Image:ghcr.io/acme/app USAGE image=ghcr.io/acme/app:1.0\n"
|
||||
"Resource:Deployment/kapp -> Image:ghcr.io/acme/app USAGE image=ghcr.io/acme/app:1.0";
|
||||
bool ok = got && strcmp(got, want) == 0;
|
||||
if (!ok) {
|
||||
fprintf(stderr, "image edges got:\n%s\nwant:\n%s\n", got ? got : "(null)", want);
|
||||
}
|
||||
free(got);
|
||||
if (!ok) {
|
||||
FAIL("no values read through a symlink out of the repository");
|
||||
}
|
||||
PASS();
|
||||
#endif
|
||||
}
|
||||
|
||||
/* A Terraform helm_release reading its values with file() of a symlink out
|
||||
* of the repository has no image of its own: the link is never followed. */
|
||||
TEST(probe_helm_terraform_values_symlink_not_followed) {
|
||||
#ifdef _WIN32
|
||||
SKIP_PLATFORM("POSIX symlink contract");
|
||||
#else
|
||||
char outside[256];
|
||||
snprintf(outside, sizeof(outside), "/tmp/cbm_gpf_out_XXXXXX");
|
||||
ASSERT_NOT_NULL(cbm_mkdtemp(outside));
|
||||
char leak[300];
|
||||
snprintf(leak, sizeof(leak), "%s/vals.yaml", outside);
|
||||
FILE *lf = fopen(leak, "wb");
|
||||
ASSERT_NOT_NULL(lf);
|
||||
fputs("image:\n repository: ghcr.io/evil/leak\n tag: \"6.6\"\n", lf);
|
||||
fclose(lf);
|
||||
const GpfFile files[] = {
|
||||
GPF_APP_CHART,
|
||||
{"tf/main.tf", "resource \"helm_release\" \"app\" {\n name = \"app\"\n"
|
||||
" chart = \"../charts/app\"\n values = [file(\"vals.yaml\")]\n}\n"}};
|
||||
const GpfFile links[] = {{"tf/vals.yaml", leak}};
|
||||
char *got = gpf_image_edges_linked(files, 4, links, 1);
|
||||
th_rmtree(outside);
|
||||
static const char want[] =
|
||||
"Chart:app -> Image:ghcr.io/acme/app DEPLOYS image=- via=chart\n"
|
||||
"Resource:Deployment/app -> Image:ghcr.io/acme/app USAGE image=ghcr.io/acme/app:1.0";
|
||||
bool ok = got && strcmp(got, want) == 0;
|
||||
if (!ok) {
|
||||
fprintf(stderr, "image edges got:\n%s\nwant:\n%s\n", got ? got : "(null)", want);
|
||||
}
|
||||
free(got);
|
||||
if (!ok) {
|
||||
FAIL("no Terraform values read through a symlink out of the repository");
|
||||
}
|
||||
PASS();
|
||||
#endif
|
||||
}
|
||||
|
||||
/* A build argument holding a secret -- by its name (*_TOKEN) or its value (a
|
||||
* GitHub token) -- is never kept on a build edge; the others still choose the
|
||||
* base image they set. */
|
||||
TEST(probe_docker_build_args_keep_no_secret) {
|
||||
static const GpfFile files[] = {
|
||||
{"web/Dockerfile", "ARG GO=1.22\nFROM golang:${GO}\n"},
|
||||
{"api/Dockerfile", "ARG PY=3.12\nFROM python:${PY}-slim\n"},
|
||||
{".env", "NPM_TOKEN=npm-secret-value\n"},
|
||||
{"compose.yaml", "services:\n web:\n image: ghcr.io/acme/web\n build:\n"
|
||||
" context: ./web\n args:\n NPM_TOKEN: ${NPM_TOKEN}\n"
|
||||
" GO: \"1.23\"\n"},
|
||||
{"scripts/build.sh",
|
||||
"#!/bin/sh\ndocker build --build-arg CI_KEY=ghp_abcdefghijklmnopqrstuvwxyz0123456789AB "
|
||||
"--build-arg PY=3.13 -t ghcr.io/acme/api:1 api\n"}};
|
||||
int secrets = gpf_query_int(files, 5,
|
||||
"SELECT count(*) FROM edges WHERE project=?1 AND "
|
||||
"(properties LIKE '%npm-secret-value%' OR properties LIKE "
|
||||
"'%ghp_abcdefghij%' OR properties LIKE '%NPM_TOKEN%')");
|
||||
int overrides = gpf_query_int(files, 5,
|
||||
"SELECT count(*) FROM edges WHERE project=?1 AND "
|
||||
"json_extract(properties,'$.override_of') IS NOT NULL");
|
||||
ASSERT_EQ(secrets, 0);
|
||||
ASSERT_EQ(overrides, 2);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Build arguments of any length are kept whole on the build edge (never cut
|
||||
* to malformed JSON): the one choosing the base still makes its edge. */
|
||||
TEST(probe_docker_long_build_args_kept_whole) {
|
||||
enum { ARGS = 40, VAL = 150, CAP = ARGS * (VAL + 40) + 512 };
|
||||
char *script = malloc(CAP);
|
||||
ASSERT_NOT_NULL(script);
|
||||
int o = snprintf(script, CAP, "#!/bin/sh\ndocker build");
|
||||
char val[VAL + 1];
|
||||
memset(val, 'v', VAL);
|
||||
val[VAL] = '\0';
|
||||
for (int i = 0; i < ARGS; i++) {
|
||||
o += snprintf(script + o, (size_t)(CAP - o), " --build-arg LABEL%d=%s", i, val);
|
||||
}
|
||||
snprintf(script + o, (size_t)(CAP - o), " --build-arg PY=3.13 -t ghcr.io/acme/api:1 api\n");
|
||||
const GpfFile files[] = {{"api/Dockerfile", "ARG PY=3.12\nFROM python:${PY}-slim\n"},
|
||||
{"scripts/build.sh", script}};
|
||||
int overrides =
|
||||
gpf_query_int(files, 2,
|
||||
"SELECT count(*) FROM edges e JOIN nodes t ON t.id=e.target_id WHERE "
|
||||
"e.project=?1 AND t.name='python' AND "
|
||||
"json_extract(e.properties,'$.override_of') IS NOT NULL");
|
||||
free(script);
|
||||
ASSERT_EQ(overrides, 1);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A template's `include "name"` calls the define of that name in its chart
|
||||
* family -- the chart and its subcharts share one template namespace -- never
|
||||
* a same-named define of an unrelated chart; a define may also be written in
|
||||
@@ -6313,6 +7102,26 @@ SUITE(grammar_probe_f) {
|
||||
RUN_TEST(probe_terraform_helm_release_deploys_its_chart);
|
||||
RUN_TEST(probe_helm_templated_objects_are_resources);
|
||||
RUN_TEST(probe_helm_chart_deploys_its_objects);
|
||||
RUN_TEST(probe_helm_chart_crds_and_werf_chart);
|
||||
RUN_TEST(probe_docker_default_images_marked_and_overrides);
|
||||
RUN_TEST(probe_helm_template_images_render_with_defaults);
|
||||
RUN_TEST(probe_helm_release_images);
|
||||
RUN_TEST(probe_helm_release_terraform_number_set);
|
||||
RUN_TEST(probe_helm_release_flux_unread_sources);
|
||||
RUN_TEST(probe_helm_release_kustomize_unread_sources);
|
||||
RUN_TEST(probe_helm_release_terraform_dynamic_set);
|
||||
RUN_TEST(probe_helm_release_terraform_sensitive_and_postrender);
|
||||
RUN_TEST(probe_helm_release_terraform_sensitive_read_earlier);
|
||||
RUN_TEST(probe_helm_release_terraform_postrender_forms);
|
||||
RUN_TEST(probe_helm_checksum_include_by_base_path);
|
||||
RUN_TEST(probe_helm_release_kustomize_transformers);
|
||||
RUN_TEST(probe_helm_empty_values_file_renders);
|
||||
RUN_TEST(probe_helm_aliased_subchart_chart_name);
|
||||
RUN_TEST(probe_helm_list_path_mark_on_chart_edge);
|
||||
RUN_TEST(probe_helm_values_symlink_not_followed);
|
||||
RUN_TEST(probe_helm_terraform_values_symlink_not_followed);
|
||||
RUN_TEST(probe_docker_build_args_keep_no_secret);
|
||||
RUN_TEST(probe_docker_long_build_args_kept_whole);
|
||||
RUN_TEST(probe_helm_include_resolves_in_the_chart_family);
|
||||
RUN_TEST(probe_k8s_selector_stays_in_its_directory);
|
||||
RUN_TEST(probe_k8s_selector_within_kustomize_build);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user