mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
feat(deploy): render Helm images, mark default-valued images, link crds and werf charts
Four gaps of the deployment graph, closed together.
Images named from a default value keep their Image; the edge says so.
A Dockerfile FROM / COPY --from filled by an ARG default (an ARG chain
names each ARG) carries default_of, as do compose `${X:-d}` images and
`docker build -t ${IMAGE:-x}` tags. An override found in the repository
adds its own edge: compose build.args and --build-arg make the Image the
build names DEPEND_ON the base they choose {override_of}; compose's .env
replacing an inline default marks the effective edge override_of and keeps
the default as {ref: image_default}, an image the service does not run.
Helm template images render (helm_template.c): a bounded Go
text/template + Sprig evaluator over the vendored gotmpl grammar, with
values read the way Helm reads them (go-yaml v2 scalars, float64 numbers,
Helm's coalesce). Each templated container image renders from its chart
family's defaults -- include / define helpers, with / range / if, subchart
values under their alias, global -- and 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, a
function not modelled) names no Image; a rendered value is checked against
the image reference grammar first. Releases render their charts with their
values: a Flux HelmRelease's spec.values, a kustomize helmCharts entry's
valuesFile + valuesInline (a chart in chartHome now links locally), a
Terraform helm_release's values + set (literals only; set_sensitive is
never read) -- each image that differs is the release's own DEPLOYS
{override_of}. A release of a chart outside the repository names images by
convention only where its values' `repository` names a registry host
{inferred: values}; a host-less one names none.
A chart DEPLOYS the CRDs of its crds/ {phase: crds}. A werf `.helm` dir
beside werf.yaml is a chart named after the werf project.
Incremental runs: a werf.yaml edit and a werf chart's values / templates
decline like a Chart.yaml chart's; a release's values file (a kustomize
valuesFile, a Terraform file()) declines too -- Terraform's now IMPORTS
{local_name: values} it, like kustomize's valuesFile.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
9f85f1043a
commit
d21949ef2c
14 files changed
+5976
-129
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 \
|
||||
|
||||
+5
-1
@@ -719,7 +719,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.
|
||||
@@ -758,6 +760,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,83 @@
|
||||
/*
|
||||
* 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. */
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const char *version;
|
||||
const char *app_version;
|
||||
} 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 */
|
||||
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). Values paths are dot-joined keys, a key's own `.` escaped
|
||||
* (`\.`), as --set writes them. */
|
||||
void 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 */
|
||||
+84
-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,46 @@ 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) or with an unknown one 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) {
|
||||
continue;
|
||||
}
|
||||
char ek[CBM_SZ_256];
|
||||
char ev[CBM_SZ_1K];
|
||||
cbm_json_escape(ek, (int)sizeof(ek), key);
|
||||
cbm_json_escape(ev, (int)sizeof(ev), 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 +1133,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) {
|
||||
|
||||
+1260
-50
File diff suppressed because it is too large.
Load diff
+134
-16
@@ -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,54 @@ 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) or an unknown word 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;
|
||||
}
|
||||
}
|
||||
char ev[CBM_SZ_1K];
|
||||
cbm_json_escape(ev, (int)sizeof(ev), 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 +2569,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 +2590,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 +2624,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 +3421,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);
|
||||
@@ -3653,8 +3769,10 @@ static int sh_link_build(cbm_pipeline_ctx_t *ctx, const sh_hit_t *x, CBMHashTabl
|
||||
sh_ci_props(x, ci, sizeof(ci));
|
||||
snprintf(props, sizeof(props),
|
||||
"{\"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);
|
||||
return SKIP_ONE;
|
||||
}
|
||||
@@ -3852,7 +3970,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,7 +22,9 @@
|
||||
#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"
|
||||
@@ -249,6 +251,402 @@ 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;
|
||||
}
|
||||
|
||||
/* An HCL literal as JSON: a string without interpolation, a number, a bool,
|
||||
* null, a tuple or object of those; NULL: anything else. */
|
||||
static yyjson_mut_val *tf_json(const tf_t *t, yyjson_mut_doc *doc, TSNode n) {
|
||||
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_64];
|
||||
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(t, doc, c);
|
||||
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(t, doc, kn);
|
||||
}
|
||||
yyjson_mut_val *val = tf_json(t, doc, vn);
|
||||
if (!key || !yyjson_mut_is_str(key) || !val) {
|
||||
return NULL;
|
||||
}
|
||||
(void)yyjson_mut_obj_put(obj, key, val);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* A repository file's text (cbm_alloc), NULL when unreadable. */
|
||||
static char *tf_read_repo(const tf_t *t, const char *path, size_t *len) {
|
||||
char full[CBM_SZ_4K];
|
||||
snprintf(full, sizeof(full), "%s/%s", t->ctx->repo_path, path);
|
||||
FILE *f = cbm_fopen(full, "rb");
|
||||
if (!f) {
|
||||
return NULL;
|
||||
}
|
||||
char *buf = NULL;
|
||||
size_t cap = 0;
|
||||
char chunk[CBM_SZ_4K];
|
||||
size_t r;
|
||||
*len = 0;
|
||||
while ((r = fread(chunk, SKIP_ONE, sizeof(chunk), f)) > 0) {
|
||||
if (*len + r + SKIP_ONE > cap) {
|
||||
cap = (*len + r + SKIP_ONE) * PAIR_LEN;
|
||||
char *g = cbm_realloc(CBM_MEM_CLASS_OTHER, buf, cap);
|
||||
if (!g) {
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, buf);
|
||||
(void)fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
buf = g;
|
||||
}
|
||||
memcpy(buf + *len, chunk, r);
|
||||
*len += r;
|
||||
}
|
||||
(void)fclose(f);
|
||||
if (buf) {
|
||||
buf[*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) 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;
|
||||
}
|
||||
char vbuf[CBM_SZ_64];
|
||||
const char *v = NULL;
|
||||
if (yyjson_mut_is_str(val)) {
|
||||
v = yyjson_mut_get_str(val);
|
||||
} else if (yyjson_mut_is_real(val)) {
|
||||
snprintf(vbuf, sizeof(vbuf), "%.17g", yyjson_mut_get_real(val));
|
||||
v = vbuf;
|
||||
} 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(t, doc, ts_node_named_child(at, SKIP_ONE));
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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. set_sensitive is never read. */
|
||||
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 values are not read: the release's values are partial */
|
||||
if (kl == strlen("set_list") && strncmp(t->src + kb, "set_list", kl) == 0) {
|
||||
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(t, doc, ts_node_named_child(a, SKIP_ONE));
|
||||
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;
|
||||
ctx->tf_releases[ctx->tf_release_n++] =
|
||||
(cbm_tf_release_t){src, chart, tf_release_values(t, body, &files, &nfiles)};
|
||||
/* 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 +718,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. */
|
||||
|
||||
@@ -2456,8 +2456,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, '/');
|
||||
@@ -2475,6 +2476,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;
|
||||
}
|
||||
@@ -2482,6 +2486,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
|
||||
@@ -2498,7 +2529,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
|
||||
@@ -2518,6 +2551,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) {
|
||||
|
||||
@@ -380,6 +380,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 {
|
||||
@@ -486,6 +496,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
|
||||
@@ -1060,6 +1076,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);
|
||||
@@ -1259,6 +1278,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);
|
||||
@@ -1269,10 +1289,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;
|
||||
@@ -1340,6 +1367,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
|
||||
@@ -1730,7 +1763,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);
|
||||
|
||||
@@ -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,267 @@ 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();
|
||||
}
|
||||
|
||||
/* 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 +6576,10 @@ 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_include_resolves_in_the_chart_family);
|
||||
RUN_TEST(probe_k8s_selector_stays_in_its_directory);
|
||||
RUN_TEST(probe_k8s_selector_within_kustomize_build);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "pipeline/lsp_surface.h"
|
||||
#include "pipeline/pass_lsp_cross.h"
|
||||
#include "pipeline/pipeline_internal.h"
|
||||
#include "pipeline/helm_template.h"
|
||||
#include "pipeline/artifact.h"
|
||||
#include "store/store.h"
|
||||
#include "semantic/semantic.h"
|
||||
@@ -4205,6 +4206,135 @@ TEST(incremental_helm_values_edit_keeps_chart_edge) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A werf `.helm` dir is a chart without Chart.yaml (pass_k8s.c): its values
|
||||
* file CONFIGURES the werf Chart, an edge a closure repair of the values file
|
||||
* alone does not hold, so the edit rebuilds, like one beside a Chart.yaml. */
|
||||
TEST(incremental_werf_values_edit_keeps_chart_edge) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_werfchart_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_temp_file(tmp, "werf.yaml", "project: site\nconfigVersion: 1\n");
|
||||
write_temp_file(tmp, ".helm/values.yaml", "replicas: 1\n");
|
||||
write_temp_file(tmp, ".helm/templates/svc.yaml",
|
||||
"apiVersion: v1\nkind: Service\nmetadata:\n name: web\n");
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/werfchart.db", tmp);
|
||||
static const char edge[] =
|
||||
"SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON "
|
||||
"t.id=e.target_id WHERE e.type='CONFIGURES' AND s.file_path='.helm/values.yaml' AND "
|
||||
"t.label='Chart' AND t.name='site'";
|
||||
int before = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, edge) : -1;
|
||||
write_temp_file(tmp, ".helm/values.yaml", "replicas: 3\n");
|
||||
int after = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, edge) : -1;
|
||||
cbm_incremental_route_t route = cbm_pipeline_incremental_test_last_route();
|
||||
th_rmtree(tmp);
|
||||
ASSERT_EQ(before, 1);
|
||||
ASSERT_EQ(after, 1);
|
||||
ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_FORCED_FULL);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A kustomization's helmCharts valuesFile (an IMPORTS {local_name:
|
||||
* valuesFile} target) feeds the image the release renders: the values file
|
||||
* edited alone rebuilds, and the release's image follows the edit. */
|
||||
TEST(incremental_release_values_edit_rebuilds) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_relvalues_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
write_temp_file(tmp, "kz/kustomization.yaml",
|
||||
"apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\n"
|
||||
"helmCharts:\n - name: app\n releaseName: kapp\n"
|
||||
" valuesFile: values-prod.yaml\n");
|
||||
write_temp_file(tmp, "kz/values-prod.yaml", "image:\n tag: \"2.0\"\n");
|
||||
write_temp_file(tmp, "kz/charts/app/Chart.yaml", "apiVersion: v2\nname: app\nversion: 1.0.0\n");
|
||||
write_temp_file(tmp, "kz/charts/app/values.yaml",
|
||||
"image:\n repository: ghcr.io/acme/app\n tag: \"1.0\"\n");
|
||||
write_temp_file(tmp, "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");
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/relvalues.db", tmp);
|
||||
static const char v2[] = "SELECT COUNT(*) FROM edges WHERE type='DEPLOYS' AND "
|
||||
"json_extract(properties,'$.image')='ghcr.io/acme/app:2.0'";
|
||||
static const char v3[] = "SELECT COUNT(*) FROM edges WHERE type='DEPLOYS' AND "
|
||||
"json_extract(properties,'$.image')='ghcr.io/acme/app:3.0'";
|
||||
int before = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, v2) : -1;
|
||||
write_temp_file(tmp, "kz/values-prod.yaml", "image:\n tag: \"3.0\"\n");
|
||||
int rc = k8s_inc_index(tmp, db_path);
|
||||
cbm_incremental_route_t route = cbm_pipeline_incremental_test_last_route();
|
||||
int stale = rc == 0 ? k8s_inc_count(db_path, v2) : -1;
|
||||
int after = rc == 0 ? k8s_inc_count(db_path, v3) : -1;
|
||||
th_rmtree(tmp);
|
||||
ASSERT_EQ(before, 1);
|
||||
ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_FORCED_FULL);
|
||||
ASSERT_EQ(stale, 0);
|
||||
ASSERT_EQ(after, 1);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Helm's values as its templates see them (helm_template.c): go-yaml v2
|
||||
* scalars -- YAML 1.1 booleans, every number a float64 printed like Go's %v
|
||||
* -- --set typing (int64, bool, null, a leading-zero string), coalesce where
|
||||
* a null deletes, and a field of a missing map an error: no value. */
|
||||
TEST(helm_values_follow_helm_semantics) {
|
||||
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
|
||||
ASSERT_NOT_NULL(doc);
|
||||
static const char src[] = "tag: 1.10\nbig: 20231010\nm: 1000000\nyes_: yes\noff_: Off\n"
|
||||
"nul: ~\nq: \"1.10\"\noct: 0755\nkeep: k\ngone: g\n";
|
||||
yyjson_mut_val *v = cbm_helm_values_parse(doc, src, strlen(src));
|
||||
ASSERT_NOT_NULL(v);
|
||||
char buf[64];
|
||||
ASSERT_TRUE(cbm_helm_scalar_text(yyjson_mut_obj_get(v, "tag"), buf, sizeof(buf)));
|
||||
ASSERT_STR_EQ(buf, "1.1");
|
||||
ASSERT_TRUE(cbm_helm_scalar_text(yyjson_mut_obj_get(v, "big"), buf, sizeof(buf)));
|
||||
ASSERT_STR_EQ(buf, "2.023101e+07");
|
||||
ASSERT_TRUE(cbm_helm_scalar_text(yyjson_mut_obj_get(v, "m"), buf, sizeof(buf)));
|
||||
ASSERT_STR_EQ(buf, "1e+06");
|
||||
ASSERT_TRUE(cbm_helm_scalar_text(yyjson_mut_obj_get(v, "oct"), buf, sizeof(buf)));
|
||||
ASSERT_STR_EQ(buf, "493");
|
||||
ASSERT_TRUE(yyjson_mut_is_true(yyjson_mut_obj_get(v, "yes_")));
|
||||
ASSERT_TRUE(yyjson_mut_is_false(yyjson_mut_obj_get(v, "off_")));
|
||||
ASSERT_TRUE(yyjson_mut_is_null(yyjson_mut_obj_get(v, "nul")));
|
||||
ASSERT_STR_EQ(yyjson_mut_get_str(yyjson_mut_obj_get(v, "q")), "1.10");
|
||||
ASSERT_TRUE(cbm_helm_values_set(doc, v, "a\\.b.c[1]", "007", false));
|
||||
ASSERT_TRUE(cbm_helm_values_set(doc, v, "n", "42", false));
|
||||
ASSERT_TRUE(cbm_helm_values_set(doc, v, "s", "42", true));
|
||||
yyjson_mut_val *ab = yyjson_mut_obj_get(v, "a.b");
|
||||
ASSERT_NOT_NULL(ab);
|
||||
yyjson_mut_val *c = yyjson_mut_obj_get(ab, "c");
|
||||
ASSERT_TRUE(yyjson_mut_is_arr(c));
|
||||
ASSERT_TRUE(yyjson_mut_is_null(yyjson_mut_arr_get(c, 0)));
|
||||
ASSERT_STR_EQ(yyjson_mut_get_str(yyjson_mut_arr_get(c, 1)), "007");
|
||||
ASSERT_TRUE(yyjson_mut_is_int(yyjson_mut_obj_get(v, "n")));
|
||||
ASSERT_TRUE(yyjson_mut_is_str(yyjson_mut_obj_get(v, "s")));
|
||||
static const char over[] = "gone: null\nkeep: x\nnew: 1\n";
|
||||
yyjson_mut_val *o = cbm_helm_values_parse(doc, over, strlen(over));
|
||||
yyjson_mut_val *m = cbm_helm_values_coalesce(doc, v, o);
|
||||
ASSERT_NULL(yyjson_mut_obj_get(m, "gone"));
|
||||
ASSERT_STR_EQ(yyjson_mut_get_str(yyjson_mut_obj_get(m, "keep")), "x");
|
||||
ASSERT_NOT_NULL(yyjson_mut_obj_get(m, "new"));
|
||||
ASSERT_STR_EQ(yyjson_mut_get_str(yyjson_mut_obj_get(v, "keep")), "k"); /* base untouched */
|
||||
/* a template: a missing last key renders "", a field of it is an error */
|
||||
static const char tpl[] = "image: \"{{ .Values.keep }}:{{ .Values.none }}\"\n"
|
||||
"image: \"{{ .Values.none.repo }}\"\n";
|
||||
CBMArena a;
|
||||
cbm_arena_init_lazy(&a, CBM_SZ_4K);
|
||||
cbm_helm_probe_t probes[2] = {{.line = 1}, {.line = 2}};
|
||||
cbm_helm_render(&a, "t.yaml", tpl, strlen(tpl), v, NULL, NULL, probes, 2);
|
||||
ASSERT_EQ(probes[0].n, 1);
|
||||
ASSERT_STR_EQ(probes[0].image[0], "k:");
|
||||
ASSERT_EQ(probes[1].n, 1);
|
||||
ASSERT_NULL(probes[1].image[0]);
|
||||
cbm_arena_destroy(&a);
|
||||
yyjson_mut_doc_free(doc);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A compose project's .env feeds its compose file (variables in image,
|
||||
* build): changing it is no closure repair -- the unchanged compose file's
|
||||
* services would keep the old image. It rebuilds; no stale image remains. */
|
||||
@@ -23930,6 +24060,9 @@ SUITE(pipeline_semantic_manifest_repro) {
|
||||
RUN_TEST(incremental_env_edit_replaces_scoped_envvars);
|
||||
RUN_TEST(incremental_image_split_change_rebuilds);
|
||||
RUN_TEST(incremental_helm_values_edit_keeps_chart_edge);
|
||||
RUN_TEST(incremental_werf_values_edit_keeps_chart_edge);
|
||||
RUN_TEST(incremental_release_values_edit_rebuilds);
|
||||
RUN_TEST(helm_values_follow_helm_semantics);
|
||||
RUN_TEST(incremental_compose_dotenv_change_rebuilds);
|
||||
RUN_TEST(incremental_dockerfile_edit_keeps_docker_build_edges);
|
||||
RUN_TEST(incremental_second_build_declarer_keeps_the_edge);
|
||||
|
||||
Reference in new issue
Block a user