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 against 5c5964ef: 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 on 5c5964ef by 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:
Martin Vogel
2026-10-19 01:59:48 +02:00
parent 5c5964efb9
commit 3401def5ba
7 changed files with 843 additions and 855 deletions
File diff suppressed because it is too large Load Diff
+12 -5
View File
@@ -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
View File
@@ -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;
+16 -84
View File
@@ -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++) {
+3 -4
View File
@@ -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.
+102 -8
View File
@@ -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
View File
@@ -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);