mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 08:13:37 +08:00
fix(deploy): coarse rule for Helm changes not run, BasePath includes, render memory
Review round 3 of #2677. The user decided on a rule sound by construction: a change of maps the render does not run exactly leaves every later image of that render UNKNOWN, with no exemption. Changes of maps (helm_template.c): - Under UNKNOWN control -- a skipped if / with / range body, the rest of a loop an UNKNOWN break / continue may leave, an include or template not run, a tpl not run -- any set / unset / merge (the must* forms too), any tpl, and any include / template of a name not known, of no define or template file of the chart, or of one that may change maps, leaves the images after it UNKNOWN. - An executed set / merge does too unless its target is a known map that is poisoned exactly; a merge also for a target already poisoned, the .Values root, or a map not known within it. The own provenance and the freshness scan are gone. - .Template.BasePath is modelled as Helm sets it: <chart path>/templates, the top chart's name, then /charts/<alias or name> per subchart (pass_k8s.c). An include naming one of the chart's template files by it (the checksum idiom) is scanned with the same rule; a file reached again while scanned, or no such file, may change maps. Bounds (helm_template.c): - A capture's keys are a hash set and its mark is built in one pass (one sort for the prefix check), its steps charged. - Each render works in an arena of its own; the caller keeps its results only. - The trip reason is recorded where a budget trips: an output trip is logged as output. Releases (pass_terraform.c, pass_k8s.c): - Any set_sensitive leaves the release without an image of its own (the name-overlap check is gone), and so does a postrender written as a dynamic block or an attribute. Cost on the 20 infra corpora against5c5964ef: 20 Image edges lost, 0 added, no budget trip. 5 are changes the rule rightly cannot follow (a second mustMerge into a poisoned values map; a skipped part that may change maps), 15 come from checksum-included files that hold a tpl. Two controls of rounds 1-3 now read UNKNOWN; their tests say so. Tests: one per item, each failing on5c5964efby its assertion (the BasePath ones also on the coarse rule without it); the wrong-image scenarios of rounds 2 and 3 stay as tests. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
+352
-657
File diff suppressed because it is too large
Load Diff
@@ -44,11 +44,17 @@ bool cbm_helm_values_set(yyjson_mut_doc *doc, yyjson_mut_val *root, const char *
|
||||
* 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. */
|
||||
/* A chart's metadata, as its templates see it under .Chart -- and where its
|
||||
* templates are: .Template.BasePath as Helm sets it (`<chart path>/templates`,
|
||||
* the chart path its root's name and `/charts/<name or alias>` per subchart
|
||||
* below it; NULL: not known) and the chart's directory as its template files
|
||||
* are added to the defines ("" at the repository root). */
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const char *version;
|
||||
const char *app_version;
|
||||
const char *base_path;
|
||||
const char *dir;
|
||||
} cbm_helm_chart_meta_t;
|
||||
|
||||
/* The named templates (`define` / `block`) of one chart family -- a chart and
|
||||
@@ -75,10 +81,11 @@ typedef struct {
|
||||
|
||||
/* 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. False when the render ran past its step or
|
||||
* output budget -- a template Helm would not finish either (logged): then
|
||||
* every capture is UNKNOWN. */
|
||||
* in arena a, which gets only these: the render works in an arena of its own).
|
||||
* Values paths are dot-joined keys, a key's own `.` escaped (`\.`), as --set
|
||||
* writes them. False when the render ran past its step, output or memory
|
||||
* budget -- a template Helm would not finish either (logged): then every
|
||||
* capture is UNKNOWN. */
|
||||
bool cbm_helm_render(CBMArena *a, const char *rel, const char *src, size_t len,
|
||||
yyjson_mut_val *values, const cbm_helm_chart_meta_t *chart,
|
||||
const cbm_helm_defines_t *defs, cbm_helm_probe_t *probes, int nprobes);
|
||||
|
||||
+28
-86
@@ -3920,9 +3920,6 @@ typedef struct {
|
||||
int tcap;
|
||||
k8s_himgs_t dflt; /* the containers' images with the defaults */
|
||||
int edges;
|
||||
/* the release rendered: its set_sensitive names (JSON array; NULL: none) --
|
||||
* an image reading one is not known, its value never read */
|
||||
const char *sensitive;
|
||||
} k8s_helm_t;
|
||||
|
||||
/* The templated images of the facts, kept past them. */
|
||||
@@ -4319,9 +4316,9 @@ static void k8s_helm_take(cbm_pipeline_ctx_t *ctx, const cbm_helm_probe_t *p, in
|
||||
/* The images of templated containers [from, to) of one template, rendered
|
||||
* with `values` (NULL: the chart's own). */
|
||||
static void k8s_helm_render(cbm_pipeline_ctx_t *ctx, k8s_helm_t *h, k8s_bundles_t *b,
|
||||
const k8s_hchart_t *c, const char *as_name, const k8s_timg_t *t,
|
||||
int from, int to, yyjson_mut_val *values, const char *src, size_t len,
|
||||
cbm_helm_probe_t *probes, int np) {
|
||||
const k8s_hchart_t *c, const char *as_name, const char *full,
|
||||
const k8s_timg_t *t, int from, int to, yyjson_mut_val *values,
|
||||
const char *src, size_t len, cbm_helm_probe_t *probes, int np) {
|
||||
char fam[CBM_SZ_1K];
|
||||
k8s_chart_family(b, c->dir, fam, sizeof(fam));
|
||||
const cbm_helm_defines_t *defs = k8s_helm_defines(ctx, b, h, fam);
|
||||
@@ -4329,6 +4326,9 @@ static void k8s_helm_render(cbm_pipeline_ctx_t *ctx, k8s_helm_t *h, k8s_bundles_
|
||||
* getAliasDependency) */
|
||||
cbm_helm_chart_meta_t meta = c->meta;
|
||||
meta.name = as_name ? as_name : meta.name;
|
||||
/* .Template.BasePath: the chart's path (full) and templates/, as Helm */
|
||||
meta.base_path = full ? cbm_arena_sprintf(&h->a, "%s/templates", full) : NULL;
|
||||
meta.dir = c->dir;
|
||||
cbm_helm_render(&h->a, t[from].file, src, len, values ? values : c->values, &meta, defs, probes,
|
||||
np);
|
||||
(void)to;
|
||||
@@ -4354,68 +4354,8 @@ static void k8s_helm_emit_uses(cbm_pipeline_ctx_t *ctx, k8s_helm_t *h, const k8s
|
||||
}
|
||||
}
|
||||
|
||||
/* Do values paths a and b overlap -- one is the other or lies within it
|
||||
* (a key boundary `.` / `[` after the shorter; "" is the whole values)? */
|
||||
static bool k8s_path_overlap(const char *a, const char *b) {
|
||||
size_t al = strlen(a);
|
||||
size_t bl = strlen(b);
|
||||
size_t m = al < bl ? al : bl;
|
||||
if (strncmp(a, b, m) != 0) {
|
||||
return false;
|
||||
}
|
||||
char c = (al < bl ? b : a)[m];
|
||||
return m == 0 || c == '\0' || c == '.' || c == '[';
|
||||
}
|
||||
|
||||
/* Does values path `path` (of the release's chart) overlap a set_sensitive
|
||||
* name of the release rendered? */
|
||||
static bool k8s_helm_sensitive_path(const k8s_helm_t *h, const char *path) {
|
||||
if (!h->sensitive || !path) {
|
||||
return false;
|
||||
}
|
||||
yyjson_doc *d = yyjson_read(h->sensitive, strlen(h->sensitive), 0);
|
||||
bool hit = !d; /* names not read: say it touches */
|
||||
size_t idx, max;
|
||||
yyjson_val *k;
|
||||
yyjson_arr_foreach(d ? yyjson_doc_get_root(d) : NULL, idx, max, k) {
|
||||
const char *s = yyjson_get_str(k);
|
||||
hit = hit || (s && k8s_path_overlap(path, s));
|
||||
}
|
||||
if (d) {
|
||||
yyjson_doc_free(d);
|
||||
}
|
||||
return hit;
|
||||
}
|
||||
|
||||
/* Does an image reading values keys `keys` (a JSON array, of its chart at
|
||||
* key path `prefix` within the release's; global.* are the release's own)
|
||||
* read a set_sensitive name of the release rendered? */
|
||||
static bool k8s_helm_sensitive_keys(k8s_helm_t *h, const char *keys, const char *prefix) {
|
||||
if (!h->sensitive || !keys) {
|
||||
return false;
|
||||
}
|
||||
yyjson_doc *d = yyjson_read(keys, strlen(keys), 0);
|
||||
bool hit = !d;
|
||||
size_t idx, max;
|
||||
yyjson_val *k;
|
||||
yyjson_arr_foreach(d ? yyjson_doc_get_root(d) : NULL, idx, max, k) {
|
||||
const char *key = yyjson_get_str(k);
|
||||
bool global = key && strncmp(key, "global.", strlen("global.")) == 0;
|
||||
const char *full = !key ? NULL
|
||||
: global || !prefix || !prefix[0]
|
||||
? key
|
||||
: cbm_arena_sprintf(&h->a, "%s.%s", prefix, key);
|
||||
hit = hit || !full || k8s_helm_sensitive_path(h, full);
|
||||
}
|
||||
if (d) {
|
||||
yyjson_doc_free(d);
|
||||
}
|
||||
return hit;
|
||||
}
|
||||
|
||||
/* Chart `from` -DEPLOYS {via, ref: image, image, override_of}-> the images an
|
||||
* overlay changed: rendered there, absent from the defaults' images. An image
|
||||
* reading a set_sensitive name (whose value is not read) is not known. */
|
||||
* overlay changed: rendered there, absent from the defaults' images. */
|
||||
static void k8s_helm_emit_overrides(cbm_pipeline_ctx_t *ctx, k8s_helm_t *h, int64_t from,
|
||||
const char *via, const k8s_himgs_t *dflt,
|
||||
const k8s_himgs_t *got, yyjson_mut_val *overlay,
|
||||
@@ -4429,7 +4369,7 @@ static void k8s_helm_emit_overrides(cbm_pipeline_ctx_t *ctx, k8s_helm_t *h, int6
|
||||
strcmp(dflt->v[j].ref, x->ref) == 0;
|
||||
}
|
||||
const char *ovr = k8s_helm_overridden(h, x->keys, overlay, globals, prefix);
|
||||
if (same || !ovr || from <= 0 || k8s_helm_sensitive_keys(h, x->keys, prefix)) {
|
||||
if (same || !ovr || from <= 0) {
|
||||
continue;
|
||||
}
|
||||
k8s_himg_t e = {from, x->img, x->ref, NULL, ovr};
|
||||
@@ -4520,12 +4460,14 @@ static bool k8s_helm_disabled(k8s_helm_t *h, yyjson_mut_val *dep, yyjson_mut_val
|
||||
* them, each subchart's defaults with its parent's `<alias>:` values and
|
||||
* `global:` coalesced over them (Helm's coalesce; a subchart always has a
|
||||
* `global`). *path gets dir's key path from top ("" for top itself), *as_name
|
||||
* the name dir's chart is included under (its alias; NULL for top). NULL: a
|
||||
* dependency condition disables it, or a values file does not parse. */
|
||||
* the name dir's chart is included under (its alias; NULL for top), *full its
|
||||
* chart path as Helm's ChartFullPath (top's name, then `/charts/<as_name>`
|
||||
* per chart down; NULL: not known). NULL: a dependency condition disables it,
|
||||
* or a values file does not parse. */
|
||||
static yyjson_mut_val *k8s_helm_context(cbm_pipeline_ctx_t *ctx, k8s_helm_t *h, k8s_bundles_t *b,
|
||||
k8s_dep_link_t *l, const char *dir, const char *top,
|
||||
yyjson_mut_val *top_vals, const char **path,
|
||||
const char **as_name) {
|
||||
const char **as_name, const char **full) {
|
||||
const char **anc = NULL;
|
||||
int na = k8s_helm_ancestors(h, b, dir, &anc);
|
||||
int ti = -SKIP_ONE; /* top's index among the ancestors; na: dir itself */
|
||||
@@ -4538,6 +4480,7 @@ static yyjson_mut_val *k8s_helm_context(cbm_pipeline_ctx_t *ctx, k8s_helm_t *h,
|
||||
}
|
||||
*path = "";
|
||||
*as_name = NULL;
|
||||
*full = NULL;
|
||||
if (ti < 0) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -4545,6 +4488,7 @@ static yyjson_mut_val *k8s_helm_context(cbm_pipeline_ctx_t *ctx, k8s_helm_t *h,
|
||||
if (!cur_chart || !cur_chart->values) {
|
||||
return NULL;
|
||||
}
|
||||
*full = cur_chart->meta.name;
|
||||
yyjson_mut_val *cur = top_vals ? cbm_helm_values_coalesce(h->doc, cur_chart->values, top_vals)
|
||||
: cur_chart->values;
|
||||
for (int i = (ti == na ? na : ti) - SKIP_ONE; i >= -SKIP_ONE && ti != na; i--) {
|
||||
@@ -4564,6 +4508,7 @@ static yyjson_mut_val *k8s_helm_context(cbm_pipeline_ctx_t *ctx, k8s_helm_t *h,
|
||||
*path = (*path)[0] ? cbm_arena_sprintf(&h->a, "%s.%s", *path, alias)
|
||||
: cbm_arena_strdup(&h->a, alias);
|
||||
*as_name = alias;
|
||||
*full = *full ? cbm_arena_sprintf(&h->a, "%s/charts/%s", *full, alias) : NULL;
|
||||
yyjson_mut_val *sub = yyjson_mut_is_obj(cur) ? yyjson_mut_obj_get(cur, alias) : NULL;
|
||||
yyjson_mut_val *next = cbm_helm_values_coalesce(h->doc, child->values, sub);
|
||||
yyjson_mut_val *pg = yyjson_mut_is_obj(cur) ? yyjson_mut_obj_get(cur, "global") : NULL;
|
||||
@@ -4583,10 +4528,10 @@ static yyjson_mut_val *k8s_helm_context(cbm_pipeline_ctx_t *ctx, k8s_helm_t *h,
|
||||
/* Render the templated containers [from, to) of one template with `vals`
|
||||
* and take each container's images. */
|
||||
static void k8s_helm_images_of(cbm_pipeline_ctx_t *ctx, k8s_helm_t *h, k8s_bundles_t *b,
|
||||
const k8s_hchart_t *c, const char *as_name, int from, int to,
|
||||
yyjson_mut_val *vals, const char *src, size_t len,
|
||||
const k8s_hchart_t *c, const char *as_name, const char *full,
|
||||
int from, int to, yyjson_mut_val *vals, const char *src, size_t len,
|
||||
cbm_helm_probe_t *probes, int np, k8s_himgs_t *out) {
|
||||
k8s_helm_render(ctx, h, b, c, as_name, h->t, from, to, vals, src, len, probes, np);
|
||||
k8s_helm_render(ctx, h, b, c, as_name, full, h->t, from, to, vals, src, len, probes, np);
|
||||
for (int i = from; i < to; i++) {
|
||||
k8s_helm_take(ctx, probes, np, h->t[i].start_line, h->t[i].end_line, h->t[i].node,
|
||||
h->t[i].container, out);
|
||||
@@ -4614,18 +4559,19 @@ static void k8s_helm_link_defaults(cbm_pipeline_ctx_t *ctx, k8s_bundles_t *b, k8
|
||||
: NULL;
|
||||
const char *path = NULL;
|
||||
const char *as_name = NULL;
|
||||
const char *full = NULL;
|
||||
yyjson_mut_val *vals = NULL;
|
||||
if (c) {
|
||||
k8s_chart_family(b, c->dir, fam, sizeof(fam));
|
||||
vals = k8s_helm_context(ctx, h, b, &l, c->dir, fam, NULL, &path, &as_name);
|
||||
vals = k8s_helm_context(ctx, h, b, &l, c->dir, fam, NULL, &path, &as_name, &full);
|
||||
}
|
||||
int len = 0;
|
||||
char *src = vals ? k8s_repo_read(ctx, h->t[from].file, &len) : NULL;
|
||||
cbm_helm_probe_t *probes = NULL;
|
||||
int np = src ? k8s_helm_probes(h, h->t, from, to, src, (size_t)len, &probes) : 0;
|
||||
if (np > 0) {
|
||||
k8s_helm_images_of(ctx, h, b, c, as_name, from, to, vals, src, (size_t)len, probes, np,
|
||||
&uses);
|
||||
k8s_helm_images_of(ctx, h, b, c, as_name, full, from, to, vals, src, (size_t)len,
|
||||
probes, np, &uses);
|
||||
}
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, src);
|
||||
from = to;
|
||||
@@ -4680,16 +4626,17 @@ static void k8s_helm_release_local(cbm_pipeline_ctx_t *ctx, k8s_bundles_t *b, k8
|
||||
k8s_hchart_t *c = in ? k8s_helm_chart(ctx, h, l, dir) : NULL;
|
||||
const char *path = NULL;
|
||||
const char *as_name = NULL;
|
||||
const char *full = NULL;
|
||||
yyjson_mut_val *vals =
|
||||
c ? k8s_helm_context(ctx, h, b, l, dir, top, rv, &path, &as_name) : NULL;
|
||||
c ? k8s_helm_context(ctx, h, b, l, dir, top, rv, &path, &as_name, &full) : NULL;
|
||||
int len = 0;
|
||||
char *srcs = vals ? k8s_repo_read(ctx, h->t[from].file, &len) : NULL;
|
||||
cbm_helm_probe_t *probes = NULL;
|
||||
int np = srcs ? k8s_helm_probes(h, h->t, from, to, srcs, (size_t)len, &probes) : 0;
|
||||
if (np > 0) {
|
||||
k8s_himgs_t got = {0};
|
||||
k8s_helm_images_of(ctx, h, b, c, as_name, from, to, vals, srcs, (size_t)len, probes, np,
|
||||
&got);
|
||||
k8s_helm_images_of(ctx, h, b, c, as_name, full, from, to, vals, srcs, (size_t)len,
|
||||
probes, np, &got);
|
||||
yyjson_mut_val *overlay = path && path[0] ? k8s_helm_dig(rv, path) : rv;
|
||||
yyjson_mut_val *globals = yyjson_mut_obj_get(rv, "global");
|
||||
k8s_helm_emit_overrides(ctx, h, src, via, &h->dflt, &got, overlay, globals,
|
||||
@@ -4748,9 +4695,7 @@ static void k8s_helm_convention(cbm_pipeline_ctx_t *ctx, k8s_helm_t *h, int64_t
|
||||
: "",
|
||||
dig[0] ? dig : tag)
|
||||
: NULL;
|
||||
/* a set_sensitive name within the mapping: its value is not read */
|
||||
bool sensitive = k8s_helm_sensitive_path(h, path ? path : "");
|
||||
int64_t img = ref && !sensitive && k8s_helm_has_host(ref) && cbm_image_ref_valid(ref)
|
||||
int64_t img = ref && k8s_helm_has_host(ref) && cbm_image_ref_valid(ref)
|
||||
? cbm_pipeline_image_node(ctx, ref)
|
||||
: 0;
|
||||
int before = emitted->n;
|
||||
@@ -4987,12 +4932,9 @@ static void k8s_helm_link_releases(cbm_pipeline_ctx_t *ctx, k8s_bundles_t *b, k8
|
||||
if (jd) {
|
||||
yyjson_doc_free(jd);
|
||||
}
|
||||
h->sensitive = r->sensitive;
|
||||
k8s_helm_release(ctx, b, h, &l, r->src, "terraform", r->chart, rv);
|
||||
h->sensitive = NULL;
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, r->values);
|
||||
}
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, r->sensitive);
|
||||
}
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, ctx->tf_releases);
|
||||
ctx->tf_releases = NULL;
|
||||
|
||||
@@ -645,8 +645,8 @@ static yyjson_mut_val *tf_set_block(const tf_t *t, yyjson_mut_doc *doc, TSNode b
|
||||
return entry;
|
||||
}
|
||||
|
||||
/* Is block `a` a `dynamic "set"` / `"set_list"` / `"set_sensitive"` --
|
||||
* entries (and names) made by a for_each, never literal? */
|
||||
/* Is block `a` a `dynamic "set"` / `"set_list"` / `"set_sensitive"` /
|
||||
* `"postrender"` -- made by a for_each, never literal? */
|
||||
static bool tf_dynamic_set(const tf_t *t, TSNode a) {
|
||||
TSNode kw = ts_node_named_child(a, 0);
|
||||
TSNode label = ts_node_named_child(a, SKIP_ONE);
|
||||
@@ -663,7 +663,8 @@ static bool tf_dynamic_set(const tf_t *t, TSNode a) {
|
||||
return (ll == strlen("\"set\"") && strncmp(t->src + lb, "\"set\"", ll) == 0) ||
|
||||
(ll == strlen("\"set_list\"") && strncmp(t->src + lb, "\"set_list\"", ll) == 0) ||
|
||||
(ll == strlen("\"set_sensitive\"") &&
|
||||
strncmp(t->src + lb, "\"set_sensitive\"", ll) == 0);
|
||||
strncmp(t->src + lb, "\"set_sensitive\"", ll) == 0) ||
|
||||
(ll == strlen("\"postrender\"") && strncmp(t->src + lb, "\"postrender\"", ll) == 0);
|
||||
}
|
||||
|
||||
/* Is node n an identifier spelling `word`? */
|
||||
@@ -676,57 +677,15 @@ static bool tf_word(const tf_t *t, TSNode n, const char *word) {
|
||||
return l == strlen(word) && strncmp(t->src + b, word, l) == 0;
|
||||
}
|
||||
|
||||
/* The expression of attribute `name` of a block's body or of an HCL object
|
||||
* (`{ name = ..., value = ... }`); a null node when there is none. */
|
||||
static TSNode tf_entry_attr(const tf_t *t, TSNode entry, const char *name) {
|
||||
TSNode e = tf_unwrap(entry);
|
||||
for (uint32_t j = 0; j < ts_node_named_child_count(e); j++) {
|
||||
TSNode c = ts_node_named_child(e, j);
|
||||
if (tf_type(e, "block") && tf_type(c, "body")) {
|
||||
for (uint32_t k = 0; k < ts_node_named_child_count(c); k++) {
|
||||
TSNode at = ts_node_named_child(c, k);
|
||||
if (tf_type(at, "attribute") && ts_node_named_child_count(at) >= PAIR_LEN &&
|
||||
tf_word(t, ts_node_named_child(at, 0), name)) {
|
||||
return ts_node_named_child(at, SKIP_ONE);
|
||||
}
|
||||
}
|
||||
} else if (tf_type(e, "object") && tf_type(c, "object_elem")) {
|
||||
TSNode kn = tf_unwrap(ts_node_child_by_field_name(c, "key", (uint32_t)strlen("key")));
|
||||
TSNode id = tf_type(kn, "variable_expr") ? ts_node_named_child(kn, 0) : (TSNode){0};
|
||||
if (tf_word(t, id, name)) {
|
||||
return ts_node_child_by_field_name(c, "val", (uint32_t)strlen("val"));
|
||||
}
|
||||
}
|
||||
}
|
||||
return (TSNode){0};
|
||||
}
|
||||
|
||||
/* The literal name of set_sensitive entry `entry` (a block, or an object of
|
||||
* the provider v3 list) appended to names -- the name only, the value is
|
||||
* never read; false when the name is no literal. */
|
||||
static bool tf_sensitive_name(const tf_t *t, yyjson_mut_doc *doc, yyjson_mut_val *names,
|
||||
TSNode entry) {
|
||||
yyjson_mut_val *nm = tf_json(t, doc, tf_entry_attr(t, entry, "name"));
|
||||
if (!yyjson_mut_is_str(nm)) {
|
||||
return false;
|
||||
}
|
||||
yyjson_mut_arr_append(names, nm);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* The values a helm_release body gives its chart -- `values` items
|
||||
* coalesced in order, then each `set` -- as JSON (cbm_alloc); NULL when a
|
||||
* part is no literal, is made by a `dynamic` block, or a postrender may
|
||||
* rewrite what it deploys. set_sensitive values are never read: their names
|
||||
* go to *sensitive (a JSON array, cbm_alloc; NULL: none), and an image that
|
||||
* reads one is not known (pass_k8s.c). */
|
||||
static char *tf_release_values(const tf_t *t, TSNode body, const char ***files, int *nfiles,
|
||||
char **sensitive) {
|
||||
*sensitive = NULL;
|
||||
* part is no literal, is made by a `dynamic` block, is sensitive (its value
|
||||
* is never read), or a postrender -- a block, an attribute or a dynamic one
|
||||
* -- may rewrite what it deploys. */
|
||||
static char *tf_release_values(const tf_t *t, TSNode body, const char ***files, int *nfiles) {
|
||||
yyjson_mut_doc *doc = yyjson_mut_doc_new(cbm_json_writer_allocator());
|
||||
yyjson_mut_val *root = doc ? yyjson_mut_obj(doc) : NULL;
|
||||
yyjson_mut_val *names = doc ? yyjson_mut_arr(doc) : NULL;
|
||||
bool ok = root != NULL && names != 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);
|
||||
@@ -735,31 +694,14 @@ static char *tf_release_values(const tf_t *t, TSNode body, const char ***files,
|
||||
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 and dynamic set values are not read: the release's
|
||||
* values would be partial; a postrender may rewrite its images */
|
||||
if ((kl == strlen("set_list") && strncmp(t->src + kb, "set_list", kl) == 0) ||
|
||||
tf_dynamic_set(t, a) || (tf_type(a, "block") && tf_word(t, key, "postrender"))) {
|
||||
/* set_list, set_sensitive and dynamic set values are not read:
|
||||
* the release's values would be partial; a postrender may
|
||||
* rewrite its images */
|
||||
if (tf_word(t, key, "set_list") || tf_word(t, key, "set_sensitive") ||
|
||||
tf_word(t, key, "postrender") || tf_dynamic_set(t, a)) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
bool is_sensitive =
|
||||
kl == strlen("set_sensitive") && strncmp(t->src + kb, "set_sensitive", kl) == 0;
|
||||
if (pass == 0 && is_sensitive && tf_type(a, "block")) {
|
||||
ok = tf_sensitive_name(t, doc, names, a);
|
||||
continue;
|
||||
}
|
||||
if (pass == 0 && is_sensitive && tf_type(a, "attribute")) {
|
||||
/* provider v3: set_sensitive = [{ name, value }, ...] */
|
||||
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")) {
|
||||
ok = tf_sensitive_name(t, doc, names, it);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
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");
|
||||
@@ -791,15 +733,6 @@ static char *tf_release_values(const tf_t *t, TSNode body, const char ***files,
|
||||
if (ok) {
|
||||
size_t len = 0;
|
||||
out = yyjson_mut_val_write_opts(root, 0, cbm_json_writer_allocator(), &len, NULL);
|
||||
if (out && yyjson_mut_arr_size(names) > 0) {
|
||||
*sensitive =
|
||||
yyjson_mut_val_write_opts(names, 0, cbm_json_writer_allocator(), &len, NULL);
|
||||
if (!*sensitive) {
|
||||
/* the names not kept: no image of the release could be told apart */
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, out);
|
||||
out = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (doc) {
|
||||
yyjson_mut_doc_free(doc);
|
||||
@@ -821,9 +754,8 @@ static void tf_release_push(tf_t *t, int64_t src, int64_t chart, TSNode body) {
|
||||
}
|
||||
const char **files = NULL;
|
||||
int nfiles = 0;
|
||||
char *sensitive = NULL;
|
||||
char *values = tf_release_values(t, body, &files, &nfiles, &sensitive);
|
||||
ctx->tf_releases[ctx->tf_release_n++] = (cbm_tf_release_t){src, chart, values, sensitive};
|
||||
char *values = tf_release_values(t, body, &files, &nfiles);
|
||||
ctx->tf_releases[ctx->tf_release_n++] = (cbm_tf_release_t){src, chart, values};
|
||||
/* the values files it reads, like a kustomization's valuesFile: an edit of
|
||||
* one alone renders the release again (pipeline_incremental.c) */
|
||||
for (int i = 0; i < nfiles; i++) {
|
||||
|
||||
@@ -385,10 +385,9 @@ typedef struct {
|
||||
* 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 */
|
||||
char *sensitive; /* owned: JSON array of its set_sensitive names (never values); NULL: none */
|
||||
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.
|
||||
|
||||
@@ -4871,9 +4871,10 @@ TEST(probe_helm_release_terraform_dynamic_set) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A set_sensitive whose value is never read: a release image reading its
|
||||
* name (image.tag) is not known; one reading none of them still is. A
|
||||
* postrender may rewrite what a release deploys: no image of its own. */
|
||||
/* A set_sensitive's value is never read: any set_sensitive leaves the
|
||||
* release without an image of its own, whatever its name -- image.tag or
|
||||
* db.password (which kept its edge in round 3). A postrender may rewrite
|
||||
* what a release deploys: no image of its own either. */
|
||||
TEST(probe_helm_release_terraform_sensitive_and_postrender) {
|
||||
static const GpfFile files[] = {
|
||||
GPF_APP_CHART,
|
||||
@@ -4889,15 +4890,105 @@ TEST(probe_helm_release_terraform_sensitive_and_postrender) {
|
||||
"resource \"helm_release\" \"post\" {\n name = \"post\"\n"
|
||||
" chart = \"../charts/app\"\n set {\n name = \"image.tag\"\n"
|
||||
" value = \"3.3\"\n }\n postrender {\n binary_path = \"./kustomize.sh\"\n }\n}\n"}};
|
||||
if (!gpf_expect_image_edges(files, 4, GPF_APP_DEFAULTS)) {
|
||||
FAIL("any set_sensitive and a postrender make release images unknown");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A set_sensitive whose name an image reads on another line ($tag, before
|
||||
* the image line): the release still has no image of its own -- never one
|
||||
* with the chart's default tag. */
|
||||
TEST(probe_helm_release_terraform_sensitive_read_earlier) {
|
||||
static const GpfFile files[] = {
|
||||
{"charts/web/Chart.yaml", "apiVersion: v2\nname: web\nversion: 1.0.0\nappVersion: \"1.0\"\n"},
|
||||
{"charts/web/values.yaml", "image:\n repository: ghcr.io/acme/web\n"},
|
||||
{"charts/web/templates/deploy.yaml",
|
||||
"{{- $tag := .Values.image.tag | default .Chart.AppVersion }}\n"
|
||||
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: web\nspec:\n template:\n"
|
||||
" spec:\n containers:\n - name: web\n"
|
||||
" image: \"{{ .Values.image.repository }}:{{ $tag }}\"\n"},
|
||||
{"tf/main.tf",
|
||||
"resource \"helm_release\" \"web\" {\n name = \"web\"\n"
|
||||
" chart = \"../charts/web\"\n set {\n name = \"image.repository\"\n"
|
||||
" value = \"quay.io/acme/web\"\n }\n set_sensitive {\n name = \"image.tag\"\n"
|
||||
" value = var.tag\n }\n}\n"}};
|
||||
static const char want[] =
|
||||
"Chart:web -> Image:ghcr.io/acme/web DEPLOYS image=- via=chart ref=image "
|
||||
"default_of=[\"image.repository\"]\n"
|
||||
"Resource:Deployment/web -> Image:ghcr.io/acme/web USAGE image=ghcr.io/acme/web:1.0 "
|
||||
"ref=image default_of=[\"image.repository\"]";
|
||||
if (!gpf_expect_image_edges(files, 4, want)) {
|
||||
FAIL("a set_sensitive read on another line makes the release images unknown");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A postrender as a dynamic block or as an attribute (provider v3) may
|
||||
* rewrite what a release deploys, as a block does: no image of its own. */
|
||||
TEST(probe_helm_release_terraform_postrender_forms) {
|
||||
static const GpfFile files[] = {
|
||||
GPF_APP_CHART,
|
||||
{"tf/main.tf",
|
||||
"resource \"helm_release\" \"dyn\" {\n name = \"dyn\"\n"
|
||||
" chart = \"../charts/app\"\n set {\n name = \"image.tag\"\n"
|
||||
" value = \"3.3\"\n }\n dynamic \"postrender\" {\n for_each = var.post\n"
|
||||
" content {\n binary_path = postrender.value\n }\n }\n}\n"
|
||||
"resource \"helm_release\" \"attr\" {\n name = \"attr\"\n"
|
||||
" chart = \"../charts/app\"\n set = [{ name = \"image.tag\", value = \"4.4\" }]\n"
|
||||
" postrender = { binary_path = \"./kustomize.sh\" }\n}\n"}};
|
||||
if (!gpf_expect_image_edges(files, 4, GPF_APP_DEFAULTS)) {
|
||||
FAIL("a dynamic or attribute postrender makes release images unknown");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A subchart included under an alias (redis as `cache`) has .Template.BasePath
|
||||
* app/charts/cache/templates, as Helm sets it (an image here shows it). Its
|
||||
* checksum of cm.yaml names its own file, which changes no map -- not app's
|
||||
* cm.yaml, which sets a value: its image stays known, as does app's (which
|
||||
* checksums its Secret). */
|
||||
TEST(probe_helm_checksum_include_by_base_path) {
|
||||
static const GpfFile files[] = {
|
||||
{"charts/app/Chart.yaml", "apiVersion: v2\nname: app\nversion: 1.0.0\ndependencies:\n"
|
||||
" - name: redis\n version: 1.0.0\n alias: cache\n"},
|
||||
{"charts/app/values.yaml", "image:\n repository: ghcr.io/acme/app\n tag: \"1.0\"\n"},
|
||||
{"charts/app/templates/cm.yaml", "{{- $_ := set .Values.image \"tag\" \"2.0\" }}\n"
|
||||
"apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: app\n"},
|
||||
{"charts/app/templates/secret.yaml", "apiVersion: v1\nkind: Secret\nmetadata:\n name: app\n"},
|
||||
{"charts/app/templates/deploy.yaml",
|
||||
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: app\nspec:\n template:\n"
|
||||
" metadata:\n annotations:\n checksum/secret: "
|
||||
"{{ include (print $.Template.BasePath \"/secret.yaml\") . | sha256sum }}\n"
|
||||
" spec:\n containers:\n - name: app\n"
|
||||
" image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"},
|
||||
{"charts/app/charts/redis/Chart.yaml", "apiVersion: v2\nname: redis\nversion: 1.0.0\n"},
|
||||
{"charts/app/charts/redis/values.yaml",
|
||||
"image:\n repository: ghcr.io/acme/redis\n tag: \"7.0\"\n"},
|
||||
{"charts/app/charts/redis/templates/cm.yaml",
|
||||
"apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: redis\n"},
|
||||
{"charts/app/charts/redis/templates/sts.yaml",
|
||||
"apiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: redis\nspec:\n template:\n"
|
||||
" metadata:\n annotations:\n checksum/config: "
|
||||
"{{ include (print $.Template.BasePath \"/cm.yaml\") . | sha256sum }}\n"
|
||||
" spec:\n containers:\n - name: redis\n"
|
||||
" image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"
|
||||
" - name: path\n image: \"ghcr.io/{{ .Template.BasePath }}:1\"\n"}};
|
||||
static const char want[] =
|
||||
"Chart:app -> Image:ghcr.io/acme/app DEPLOYS image=- via=chart ref=image "
|
||||
"default_of=[\"image.repository\",\"image.tag\"]\n"
|
||||
"Class:resource.helm_release.pw -> Image:quay.io/acme/pw DEPLOYS "
|
||||
"image=quay.io/acme/pw:1.0 via=terraform ref=image override_of=[\"image.repository\"]\n"
|
||||
"Chart:redis -> Image:ghcr.io/acme/redis DEPLOYS image=- via=chart ref=image "
|
||||
"default_of=[\"image.repository\",\"image.tag\"]\n"
|
||||
"Chart:redis -> Image:ghcr.io/app/charts/cache/templates DEPLOYS image=- via=chart "
|
||||
"ref=image\n"
|
||||
"Resource:Deployment/app -> Image:ghcr.io/acme/app USAGE image=ghcr.io/acme/app:1.0 "
|
||||
"ref=image default_of=[\"image.repository\",\"image.tag\"]";
|
||||
if (!gpf_expect_image_edges(files, 4, want)) {
|
||||
FAIL("set_sensitive names and postrender make release images unknown");
|
||||
"ref=image default_of=[\"image.repository\",\"image.tag\"]\n"
|
||||
"Resource:StatefulSet/redis -> Image:ghcr.io/acme/redis USAGE "
|
||||
"image=ghcr.io/acme/redis:7.0 ref=image default_of=[\"image.repository\",\"image.tag\"]\n"
|
||||
"Resource:StatefulSet/redis -> Image:ghcr.io/app/charts/cache/templates USAGE "
|
||||
"image=ghcr.io/app/charts/cache/templates:1 ref=image";
|
||||
if (!gpf_expect_image_edges(files, 9, want)) {
|
||||
FAIL("a checksum include by .Template.BasePath names the chart's own file");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
@@ -7013,6 +7104,9 @@ SUITE(grammar_probe_f) {
|
||||
RUN_TEST(probe_helm_release_kustomize_unread_sources);
|
||||
RUN_TEST(probe_helm_release_terraform_dynamic_set);
|
||||
RUN_TEST(probe_helm_release_terraform_sensitive_and_postrender);
|
||||
RUN_TEST(probe_helm_release_terraform_sensitive_read_earlier);
|
||||
RUN_TEST(probe_helm_release_terraform_postrender_forms);
|
||||
RUN_TEST(probe_helm_checksum_include_by_base_path);
|
||||
RUN_TEST(probe_helm_release_kustomize_transformers);
|
||||
RUN_TEST(probe_helm_empty_values_file_renders);
|
||||
RUN_TEST(probe_helm_aliased_subchart_chart_name);
|
||||
|
||||
+330
-11
@@ -4338,14 +4338,15 @@ TEST(helm_values_follow_helm_semantics) {
|
||||
/* Render template tpl with values src (one probe per `image:` line given)
|
||||
* and return the capture of probe 0 ("<UNKNOWN>" for UNKNOWN, "<none>" when
|
||||
* nothing was captured) -- a copy, freed by the caller. */
|
||||
static char *helm_capture(const char *vals_src, const char *tpl, const cbm_helm_defines_t *defs,
|
||||
int line) {
|
||||
static char *helm_capture_chart(const char *vals_src, const char *tpl,
|
||||
const cbm_helm_defines_t *defs, const cbm_helm_chart_meta_t *meta,
|
||||
int line) {
|
||||
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
|
||||
yyjson_mut_val *v = doc ? cbm_helm_values_parse(doc, vals_src, strlen(vals_src)) : NULL;
|
||||
CBMArena a;
|
||||
cbm_arena_init_lazy(&a, CBM_SZ_4K);
|
||||
cbm_helm_probe_t probe = {.line = line};
|
||||
cbm_helm_render(&a, "t.yaml", tpl, strlen(tpl), v, NULL, defs, &probe, 1);
|
||||
cbm_helm_render(&a, "t.yaml", tpl, strlen(tpl), v, meta, defs, &probe, 1);
|
||||
char *out = strdup(probe.n == 0 ? "<none>" : probe.image[0] ? probe.image[0] : "<UNKNOWN>");
|
||||
cbm_arena_destroy(&a);
|
||||
if (doc) {
|
||||
@@ -4354,6 +4355,12 @@ static char *helm_capture(const char *vals_src, const char *tpl, const cbm_helm_
|
||||
return out;
|
||||
}
|
||||
|
||||
/* helm_capture_chart of a template of no chart. */
|
||||
static char *helm_capture(const char *vals_src, const char *tpl, const cbm_helm_defines_t *defs,
|
||||
int line) {
|
||||
return helm_capture_chart(vals_src, tpl, defs, NULL, line);
|
||||
}
|
||||
|
||||
/* A render doing far more work than any chart -- here a range over an int
|
||||
* of 20,000,000 -- stops at its step budget (helm_template.c): every capture
|
||||
* of the template is UNKNOWN, also the one made before the loop, never a
|
||||
@@ -4540,10 +4547,12 @@ TEST(helm_unknown_break_leaves_the_loop) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A change made to a new value -- fromYaml / deepCopy results, a part of one,
|
||||
* an empty dict -- reaches nothing another part holds: the images after it
|
||||
* stay known (the opentelemetry-ebpf-instrumentation configData pattern). */
|
||||
TEST(helm_change_of_a_new_value_spoils_nothing) {
|
||||
/* A change of maps the render does not run exactly leaves every image after
|
||||
* it UNKNOWN -- also one of a new value (fromYaml / deepCopy results, an empty
|
||||
* dict): no exemption (the coarse rule, decided in review round 3). Rounds 1-3
|
||||
* kept this image (the opentelemetry-ebpf-instrumentation configData
|
||||
* pattern); it is part of the rule's measured cost. */
|
||||
TEST(helm_change_of_a_new_value_spoils_after) {
|
||||
static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\ncfg: {a: 1}\n";
|
||||
static const char tpl[] =
|
||||
"{{- $config := dict -}}\n"
|
||||
@@ -4554,7 +4563,7 @@ TEST(helm_change_of_a_new_value_spoils_nothing) {
|
||||
"{{- $_ := unset (deepCopy .Values.cfg) \"a\" }}\n"
|
||||
"image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n";
|
||||
char *got = helm_capture(vals, tpl, NULL, 6);
|
||||
ASSERT_STR_EQ(got, "ghcr.io/acme/app:1.0");
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
PASS();
|
||||
}
|
||||
@@ -4661,7 +4670,9 @@ TEST(helm_unknown_break_spoils_the_rest_of_the_loop) {
|
||||
|
||||
/* A variable once made from `dict` and then (in a body an UNKNOWN condition
|
||||
* skips) redefined or given .Values.image is no new map: a set of it there
|
||||
* may change .Values. Without the redefinition the set is of the new map. */
|
||||
* may change .Values. Without the redefinition the set is of the new map --
|
||||
* still a change in a skipped body, which (the coarse rule) leaves every
|
||||
* image after it UNKNOWN; rounds 2-3 kept that one's image. */
|
||||
TEST(helm_skipped_set_after_redefinition) {
|
||||
static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\n";
|
||||
static const char redefined[] =
|
||||
@@ -4682,7 +4693,7 @@ TEST(helm_skipped_set_after_redefinition) {
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
got = helm_capture(vals, kept, NULL, 2);
|
||||
ASSERT_STR_EQ(got, "ghcr.io/acme/app:1.0");
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
PASS();
|
||||
}
|
||||
@@ -4744,6 +4755,301 @@ TEST(helm_include_with_poisoned_argument) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The keys a capture read -- 5,000 values paths -- become its mark in one
|
||||
* pass: memory linear in them, never a copy of the mark so far per key. */
|
||||
TEST(helm_capture_keys_cost_linear_memory) {
|
||||
enum { ITEMS = 5000 };
|
||||
size_t cap = (size_t)ITEMS * 8 + CBM_SZ_64;
|
||||
char *vals = malloc(cap);
|
||||
ASSERT_NOT_NULL(vals);
|
||||
size_t o = (size_t)snprintf(vals, cap, "big: [");
|
||||
for (int i = 0; i < ITEMS; i++) {
|
||||
o += (size_t)snprintf(vals + o, cap - o, "%s{x: a}", i ? "," : "");
|
||||
}
|
||||
snprintf(vals + o, cap - o, "]\n");
|
||||
static const char tpl[] = "image: \"r/{{ range .Values.big }}{{ .x }}{{ end }}\"\n";
|
||||
cbm_mem_class_flush_thread();
|
||||
size_t base = cbm_mem_class_live_bytes(CBM_MEM_CLASS_ARENA);
|
||||
cbm_mem_class_reset_peaks();
|
||||
char *got = helm_capture(vals, tpl, NULL, 1);
|
||||
cbm_mem_class_flush_thread();
|
||||
size_t peak = cbm_mem_class_peak_bytes(CBM_MEM_CLASS_ARENA);
|
||||
free(vals);
|
||||
ASSERT_NOT_NULL(got);
|
||||
bool image = strncmp(got, "r/aaaa", strlen("r/aaaa")) == 0;
|
||||
free(got);
|
||||
ASSERT_TRUE(image);
|
||||
/* about 14 bytes a key: ~70 KB of keys; a copy per key is ~175 MB */
|
||||
ASSERT_TRUE(peak - base < (size_t)CBM_SZ_32 * CBM_SZ_1K * CBM_SZ_1K);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A render's work -- here 20 copies of a 20,000-item list -- stays in an
|
||||
* arena of its own: the caller's arena grows by the render's results only,
|
||||
* so renders one after another never keep each other's work. */
|
||||
TEST(helm_render_keeps_only_its_results) {
|
||||
enum { ITEMS = 20000 };
|
||||
size_t cap = (size_t)ITEMS * 2 + CBM_SZ_64;
|
||||
char *vals = malloc(cap);
|
||||
ASSERT_NOT_NULL(vals);
|
||||
size_t o = (size_t)snprintf(vals, cap, "big: [");
|
||||
for (int i = 0; i < ITEMS; i++) {
|
||||
o += (size_t)snprintf(vals + o, cap - o, "%s1", i ? "," : "");
|
||||
}
|
||||
snprintf(vals + o, cap - o, "]\n");
|
||||
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
|
||||
yyjson_mut_val *v = doc ? cbm_helm_values_parse(doc, vals, strlen(vals)) : NULL;
|
||||
free(vals);
|
||||
static const char tpl[] = "image: \"ghcr.io/acme/app:1\"\n"
|
||||
"{{- range 20 }}{{ $x := first $.Values.big }}{{ end }}\n";
|
||||
CBMArena a;
|
||||
cbm_arena_init_lazy(&a, CBM_SZ_4K);
|
||||
cbm_helm_probe_t probe = {.line = 1};
|
||||
size_t before = cbm_arena_total(&a);
|
||||
bool ok = cbm_helm_render(&a, "t.yaml", tpl, strlen(tpl), v, NULL, NULL, &probe, 1);
|
||||
size_t kept = cbm_arena_total(&a) - before;
|
||||
bool image = ok && probe.n == 1 && probe.image[0] &&
|
||||
strcmp(probe.image[0], "ghcr.io/acme/app:1") == 0;
|
||||
cbm_arena_destroy(&a);
|
||||
if (doc) {
|
||||
yyjson_mut_doc_free(doc);
|
||||
}
|
||||
ASSERT_NOT_NULL(v);
|
||||
ASSERT_TRUE(image);
|
||||
ASSERT_TRUE(kept < (size_t)CBM_SZ_1K * CBM_SZ_1K); /* the work: ~30 MB */
|
||||
PASS();
|
||||
}
|
||||
|
||||
static char g_helm_budget_log[CBM_SZ_4K];
|
||||
|
||||
static void helm_budget_log_sink(const char *line) {
|
||||
if (!line || !strstr(line, "helm.render.budget")) {
|
||||
return;
|
||||
}
|
||||
size_t used = strlen(g_helm_budget_log);
|
||||
snprintf(g_helm_budget_log + used, sizeof(g_helm_budget_log) - used, "%s\n", line);
|
||||
}
|
||||
|
||||
/* A render its output budget stopped -- an indent that writes past 64 MiB,
|
||||
* or one too wide to try -- says so: reason=output. */
|
||||
TEST(helm_render_budget_logs_its_reason) {
|
||||
g_helm_budget_log[0] = '\0';
|
||||
CBMLogLevel prev = cbm_log_get_level();
|
||||
cbm_log_set_level(CBM_LOG_DEBUG);
|
||||
cbm_log_set_sink(helm_budget_log_sink);
|
||||
static const char big[] = "image: \"ghcr.io/acme/app{{ indent 70000000 \"\" | trim }}:1\"\n";
|
||||
char *got = helm_capture("{}\n", big, NULL, 1);
|
||||
free(got);
|
||||
static const char huge[] =
|
||||
"image: \"ghcr.io/acme/app{{ indent 9000000000000000000 \"\" | trim }}:1\"\n";
|
||||
got = helm_capture("{}\n", huge, NULL, 1);
|
||||
free(got);
|
||||
cbm_log_set_sink(NULL);
|
||||
cbm_log_set_level(prev);
|
||||
const char *first = strstr(g_helm_budget_log, "reason=output");
|
||||
ASSERT_NOT_NULL(first);
|
||||
ASSERT_NOT_NULL(strstr(first + SKIP_ONE, "reason=output"));
|
||||
ASSERT_NULL(strstr(g_helm_budget_log, "reason=memory"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A merge into .Values may change any map of the values, also one a handle
|
||||
* ($img) was taken of before: every image after it is UNKNOWN. */
|
||||
TEST(helm_merge_into_values_root) {
|
||||
static const char vals[] = "image: {repository: ghcr.io/acme/app}\n";
|
||||
static const char tpl[] =
|
||||
"{{- $img := .Values.image }}\n"
|
||||
"{{- $_ := merge .Values (dict \"image\" (dict \"tag\" \"2.0\")) }}\n"
|
||||
"image: \"{{ $img.repository }}:{{ $img.tag | default \"1.0\" }}\"\n";
|
||||
char *got = helm_capture(vals, tpl, NULL, 3);
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A set in a body an UNKNOWN condition skips, whatever its target -- here
|
||||
* the outer $vals (.Values.image), a nested range's own $vals aside --
|
||||
* leaves every image after it UNKNOWN. */
|
||||
TEST(helm_skipped_set_beside_a_nested_scope) {
|
||||
static const char vals[] =
|
||||
"image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\njobs: [a]\n";
|
||||
static const char tpl[] = "{{- $vals := .Values.image }}\n"
|
||||
"{{- if .Capabilities.APIVersions.Has \"batch/v1\" }}\n"
|
||||
"{{- range .Values.jobs }}{{- $vals := dict }}{{- end }}\n"
|
||||
"{{- $_ := set $vals \"tag\" \"2.0\" }}\n"
|
||||
"{{- end }}\n"
|
||||
"image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n";
|
||||
char *got = helm_capture(vals, tpl, NULL, 6);
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* An UNKNOWN break in a range over maps: whether the later elements ($c of
|
||||
* web) were changed is not known -- not only the current one's. */
|
||||
TEST(helm_unknown_break_spoils_later_elements) {
|
||||
static const char vals[] = "components:\n"
|
||||
" api: {repository: ghcr.io/acme/api, tag: \"1.0\"}\n"
|
||||
" web: {repository: ghcr.io/acme/web, tag: \"1.0\"}\n";
|
||||
static const char tpl[] =
|
||||
"{{- range $n, $c := .Values.components }}\n"
|
||||
"{{- if semverCompare \">=1.29-0\" $.Capabilities.KubeVersion.Version }}{{ break }}"
|
||||
"{{ end }}\n"
|
||||
"{{- $_ := set $c \"tag\" \"2.0\" }}\n"
|
||||
"{{- end }}\n"
|
||||
"image: \"{{ .Values.components.web.repository }}:{{ .Values.components.web.tag }}\"\n";
|
||||
char *got = helm_capture(vals, tpl, NULL, 5);
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A skipped range whose element variable ($c, each component) is set is no
|
||||
* change of an outer $c (a new dict): every image after it is UNKNOWN. */
|
||||
TEST(helm_skipped_range_sets_its_element) {
|
||||
static const char vals[] = "components:\n"
|
||||
" web: {repository: ghcr.io/acme/web, tag: \"1.0\"}\n";
|
||||
static const char tpl[] =
|
||||
"{{- $c := dict }}\n"
|
||||
"{{- if .Capabilities.APIVersions.Has \"x/v1\" }}{{- range $_, $c := .Values.components }}"
|
||||
"{{- $_ := set $c \"tag\" \"2.0\" }}{{- end }}{{- end }}\n"
|
||||
"image: \"{{ .Values.components.web.repository }}:{{ .Values.components.web.tag }}\"\n";
|
||||
char *got = helm_capture(vals, tpl, NULL, 3);
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A deep merge into a map that holds a values map by reference ($d.img is
|
||||
* .Values.image, put by a set) changes that values map: the image after it
|
||||
* is UNKNOWN. */
|
||||
TEST(helm_deep_merge_reaches_a_held_values_map) {
|
||||
static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\n";
|
||||
static const char tpl[] =
|
||||
"{{- $d := dict }}{{- $_ := set $d \"img\" .Values.image }}\n"
|
||||
"{{- $_ := mergeOverwrite $d (dict \"img\" (dict \"tag\" \"2.0\")) }}\n"
|
||||
"image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n";
|
||||
char *got = helm_capture(vals, tpl, NULL, 3);
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Template files of chart app (charts/app) and of its subchart redis, which
|
||||
* app's dependencies alias `cache` (charts/app/charts/redis): a Secret and a
|
||||
* ConfigMap with no change of maps, an app ConfigMap that sets a value, and
|
||||
* a file that includes itself. */
|
||||
static cbm_helm_defines_t *helm_chart_files(void) {
|
||||
static const char secret[] = "apiVersion: v1\nkind: Secret\nmetadata:\n"
|
||||
" name: {{ include \"app.name\" . }}\n";
|
||||
static const char helpers[] = "{{- define \"app.name\" -}}{{ .Chart.Name }}{{- end -}}\n";
|
||||
static const char app_cm[] = "{{- $_ := set .Values.image \"tag\" \"2.0\" }}\n"
|
||||
"apiVersion: v1\nkind: ConfigMap\n";
|
||||
static const char sub_cm[] = "apiVersion: v1\nkind: ConfigMap\nmetadata:\n"
|
||||
" name: {{ .Chart.Name }}\n";
|
||||
static const char loop[] = "{{ include \"app/templates/loop.yaml\" . }}\n";
|
||||
cbm_helm_defines_t *d = cbm_helm_defines_new();
|
||||
if (d) {
|
||||
cbm_helm_defines_add(d, "charts/app/templates/_helpers.tpl", helpers, strlen(helpers));
|
||||
cbm_helm_defines_add(d, "charts/app/templates/secret.yaml", secret, strlen(secret));
|
||||
cbm_helm_defines_add(d, "charts/app/templates/cm.yaml", app_cm, strlen(app_cm));
|
||||
cbm_helm_defines_add(d, "charts/app/templates/loop.yaml", loop, strlen(loop));
|
||||
cbm_helm_defines_add(d, "charts/app/charts/redis/templates/cm.yaml", sub_cm,
|
||||
strlen(sub_cm));
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
/* A template of chart `meta` checksumming its file `file`, then an image. */
|
||||
static char *helm_capture_checksum(const cbm_helm_defines_t *d, const cbm_helm_chart_meta_t *meta,
|
||||
const char *file) {
|
||||
static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\n";
|
||||
char tpl[CBM_SZ_512];
|
||||
snprintf(tpl, sizeof(tpl),
|
||||
"metadata:\n annotations:\n"
|
||||
" checksum: {{ include (print $.Template.BasePath \"/%s\") . | sha256sum }}\n"
|
||||
"image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n",
|
||||
file);
|
||||
return helm_capture_chart(vals, tpl, d, meta, 4);
|
||||
}
|
||||
|
||||
static const cbm_helm_chart_meta_t HELM_APP = {
|
||||
.name = "app", .base_path = "app/templates", .dir = "charts/app"};
|
||||
|
||||
/* The checksum idiom names one of the chart's own template files by its
|
||||
* .Template.BasePath: one with no change of maps in it (a Secret) spoils
|
||||
* nothing -- its output is not rendered, the image after it is known. */
|
||||
TEST(helm_checksum_include_keeps_the_image) {
|
||||
cbm_helm_defines_t *d = helm_chart_files();
|
||||
ASSERT_NOT_NULL(d);
|
||||
char *got = helm_capture_checksum(d, &HELM_APP, "secret.yaml");
|
||||
cbm_helm_defines_free(d);
|
||||
ASSERT_STR_EQ(got, "ghcr.io/acme/app:1.0");
|
||||
free(got);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* An included template file that sets a value (cm.yaml) may change any map:
|
||||
* every image after it is UNKNOWN -- beside one that does not (secret.yaml). */
|
||||
TEST(helm_include_of_a_changing_template_file) {
|
||||
cbm_helm_defines_t *d = helm_chart_files();
|
||||
ASSERT_NOT_NULL(d);
|
||||
char *clean = helm_capture_checksum(d, &HELM_APP, "secret.yaml");
|
||||
char *changing = helm_capture_checksum(d, &HELM_APP, "cm.yaml");
|
||||
cbm_helm_defines_free(d);
|
||||
ASSERT_STR_EQ(clean, "ghcr.io/acme/app:1.0");
|
||||
ASSERT_STR_EQ(changing, "<UNKNOWN>");
|
||||
free(clean);
|
||||
free(changing);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A subchart's .Template.BasePath -- app/charts/cache/templates, by the alias
|
||||
* it is included under -- names its own cm.yaml (no change), not app's (a
|
||||
* set): its image is known; app's own checksum of cm.yaml is not. */
|
||||
TEST(helm_subchart_base_path_names_its_own_file) {
|
||||
cbm_helm_defines_t *d = helm_chart_files();
|
||||
ASSERT_NOT_NULL(d);
|
||||
const cbm_helm_chart_meta_t sub = {.name = "cache",
|
||||
.base_path = "app/charts/cache/templates",
|
||||
.dir = "charts/app/charts/redis"};
|
||||
char *own = helm_capture_checksum(d, &sub, "cm.yaml");
|
||||
char *parent = helm_capture_checksum(d, &HELM_APP, "cm.yaml");
|
||||
cbm_helm_defines_free(d);
|
||||
ASSERT_STR_EQ(own, "ghcr.io/acme/app:1.0");
|
||||
ASSERT_STR_EQ(parent, "<UNKNOWN>");
|
||||
free(own);
|
||||
free(parent);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* An include naming no template file of the chart is not looked into: every
|
||||
* image after it is UNKNOWN -- beside one naming a file that is there. */
|
||||
TEST(helm_include_of_a_missing_template_file) {
|
||||
cbm_helm_defines_t *d = helm_chart_files();
|
||||
ASSERT_NOT_NULL(d);
|
||||
char *there = helm_capture_checksum(d, &HELM_APP, "secret.yaml");
|
||||
char *missing = helm_capture_checksum(d, &HELM_APP, "missing.yaml");
|
||||
cbm_helm_defines_free(d);
|
||||
ASSERT_STR_EQ(there, "ghcr.io/acme/app:1.0");
|
||||
ASSERT_STR_EQ(missing, "<UNKNOWN>");
|
||||
free(there);
|
||||
free(missing);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A template file that includes itself is never "no change": every image
|
||||
* after an include of it is UNKNOWN. */
|
||||
TEST(helm_template_file_including_itself) {
|
||||
cbm_helm_defines_t *d = helm_chart_files();
|
||||
ASSERT_NOT_NULL(d);
|
||||
char *got = helm_capture_checksum(d, &HELM_APP, "loop.yaml");
|
||||
cbm_helm_defines_free(d);
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
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. */
|
||||
@@ -24481,7 +24787,7 @@ SUITE(pipeline_semantic_manifest_repro) {
|
||||
RUN_TEST(helm_unknown_branch_spoils_what_it_assigns);
|
||||
RUN_TEST(helm_mutator_spoils_the_map_it_changes);
|
||||
RUN_TEST(helm_unknown_break_leaves_the_loop);
|
||||
RUN_TEST(helm_change_of_a_new_value_spoils_nothing);
|
||||
RUN_TEST(helm_change_of_a_new_value_spoils_after);
|
||||
RUN_TEST(helm_render_captures_grow_linearly);
|
||||
RUN_TEST(helm_render_memory_budget_ends_unknown);
|
||||
RUN_TEST(helm_skipped_set_of_a_call_result);
|
||||
@@ -24492,6 +24798,19 @@ SUITE(pipeline_semantic_manifest_repro) {
|
||||
RUN_TEST(helm_many_changed_maps_keep_reads_right);
|
||||
RUN_TEST(helm_define_memo_under_mutual_recursion);
|
||||
RUN_TEST(helm_include_with_poisoned_argument);
|
||||
RUN_TEST(helm_capture_keys_cost_linear_memory);
|
||||
RUN_TEST(helm_render_keeps_only_its_results);
|
||||
RUN_TEST(helm_render_budget_logs_its_reason);
|
||||
RUN_TEST(helm_merge_into_values_root);
|
||||
RUN_TEST(helm_skipped_set_beside_a_nested_scope);
|
||||
RUN_TEST(helm_unknown_break_spoils_later_elements);
|
||||
RUN_TEST(helm_skipped_range_sets_its_element);
|
||||
RUN_TEST(helm_deep_merge_reaches_a_held_values_map);
|
||||
RUN_TEST(helm_checksum_include_keeps_the_image);
|
||||
RUN_TEST(helm_include_of_a_changing_template_file);
|
||||
RUN_TEST(helm_subchart_base_path_names_its_own_file);
|
||||
RUN_TEST(helm_include_of_a_missing_template_file);
|
||||
RUN_TEST(helm_template_file_including_itself);
|
||||
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