mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(deploy): bound Helm render memory, no wrong image from a skipped change
Review round 2 of #2677 (N1-N12). Every case ends in an explicit unknown,
never in an OOM, a quadratic stall or a wrong image.
Bounds (helm_template.c, pass_k8s.c):
- N1: capture arrays grow by doubling, so n captures cost memory linear in
n. Arena growth since the render began counts against a third budget
(256 MiB) beside steps and output; a trip is logged with its reason.
- N10: poisoned maps are a pointer hash set (merged values a path list), so
a loop poisoning a new dict per iteration keeps reads O(1). Release images
are deduplicated by hash.
Wrong images (helm_template.c):
- N2: a set / merge in a body an UNKNOWN condition skips is judged by its
target whatever it is -- a call's result like `index .Values "image"` is
evaluated in the frame: own, or a known map (poisoned), else it taints.
- N3: putting a value another part holds into an own map revokes own; a
part of a poisoned map is never own.
- N4: a run-time set whose target is an UNKNOWN call result taints.
- N5: an UNKNOWN break / continue spoils the rest of the loop body around
it; a continue is a continue, not a break.
- N6: the scan notes redefinitions and assignments, carries what it saw
across the actions of one branch, and never judges a variable the part
rebinds by its value in the frame.
- N11: a define's "may not change" that leaned on a define still being
scanned (mutual includes) is not kept.
- N12: an include not run because its argument is poisoned or UNKNOWN
spoils that argument (poisoned deep, or a taint).
Releases and charts (pass_k8s.c, pass_terraform.c):
- N8: an empty values.yaml is values {} (Helm's), not a file not read.
- N9: a Terraform set_sensitive whose name overlaps a key an image reads, a
Terraform postrender, or a kustomization with its own transformers
(images, patches, replacements, transformers, components) leaves the
release without an image of its own.
Tests (N7): the symlink probes are POSIX-only (SKIP_PLATFORM on Windows).
One test per item, RED on 4cea54f7 by its assertion; N10's is a correctness
control, its cost measured (linear vs quadratic) outside the suite.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
4cea54f758
commit
5c5964efb9
7 files changed
+953
-192
No files matched your search
+425
-158
@@ -39,13 +39,15 @@ enum { HELM_MAX_NESTING = 1000 }; /* Helm engine's recursionMaxNums */
|
||||
enum { HELM_MAX_SET_INDEX = 65536 }; /* Helm strvals' MaxIndex for `--set a[n]` */
|
||||
|
||||
/* A render doing more than this -- steps (an action, an evaluation, a range
|
||||
* iteration) or bytes written to any sink -- is one Helm never finishes
|
||||
* iteration), bytes written to any sink, or memory it takes (the arena's
|
||||
* growth: captures, list copies, strings) -- is one Helm never finishes
|
||||
* either (a range over a huge int, defines that call each other in a fan):
|
||||
* it stops, and every probe of the template is UNKNOWN. Counted, never timed;
|
||||
* far above any real chart (the 20 infra corpora and the helm oracle charts
|
||||
* never reach it). */
|
||||
enum { HELM_MAX_STEPS = 10000000 };
|
||||
static const size_t HELM_MAX_OUTPUT = (size_t)CBM_SZ_64 * CBM_SZ_1K * CBM_SZ_1K; /* 64 MiB */
|
||||
static const size_t HELM_MAX_OUTPUT = (size_t)CBM_SZ_64 * CBM_SZ_1K * CBM_SZ_1K; /* 64 MiB */
|
||||
static const size_t HELM_MAX_MEMORY = (size_t)CBM_SZ_256 * CBM_SZ_1K * CBM_SZ_1K; /* 256 MiB */
|
||||
|
||||
/* ── Values: YAML -> JSON ─────────────────────────────────────────── */
|
||||
|
||||
@@ -1364,12 +1366,6 @@ typedef struct {
|
||||
hsink_t *out; /* NULL: the template's own output, which the probes read */
|
||||
} hframe_t;
|
||||
|
||||
typedef struct {
|
||||
const void *map; /* the hv_map_t / yyjson object changed */
|
||||
const char *path; /* its values path (NULL: none) */
|
||||
bool deep; /* merge: every map within it may have changed too */
|
||||
} hpoison_t;
|
||||
|
||||
typedef struct {
|
||||
CBMArena *a;
|
||||
yyjson_mut_doc *scratch;
|
||||
@@ -1382,14 +1378,16 @@ typedef struct {
|
||||
bool cont;
|
||||
long steps;
|
||||
size_t written; /* bytes appended to any sink */
|
||||
bool stop; /* over HELM_MAX_STEPS / HELM_MAX_OUTPUT: the render is UNKNOWN */
|
||||
size_t arena0; /* the arena's total when the render began */
|
||||
bool stop; /* over HELM_MAX_STEPS / _OUTPUT / _MEMORY: the render is UNKNOWN */
|
||||
/* maps a set / unset / merge changed in place (not modelled): what they
|
||||
* hold is UNKNOWN from then on */
|
||||
hpoison_t *poison;
|
||||
int npoison;
|
||||
int pcap;
|
||||
* hold is UNKNOWN from then on -- by address ("%p" keys in the arena),
|
||||
* and for a merge of values also by values path (everything below it) */
|
||||
CBMHashTable *poison_maps;
|
||||
CBMHashTable *poison_paths;
|
||||
bool tainted; /* a change of unknown target: every capture ending later is UNKNOWN */
|
||||
CBMHashTable *mutates; /* define name -> it may change a map ((void *)1 / (void *)2) */
|
||||
CBMHashTable *mutates; /* define name -> it may change a map (he_define_may_mutate) */
|
||||
long busy_hits; /* defines met again while being scanned */
|
||||
/* probes of the top-level template */
|
||||
const char *top_src;
|
||||
size_t top_len;
|
||||
@@ -1408,9 +1406,14 @@ typedef struct {
|
||||
int tcap;
|
||||
} hst_t;
|
||||
|
||||
/* Is the render over its memory budget (the arena grown since it began)? */
|
||||
static bool hst_over_memory(const hst_t *st) {
|
||||
return cbm_arena_total(st->a) - st->arena0 > HELM_MAX_MEMORY;
|
||||
}
|
||||
|
||||
/* One step of work; false once the render is over its budget. */
|
||||
static bool hst_step(hst_t *st) {
|
||||
if (!st->stop && ++st->steps > HELM_MAX_STEPS) {
|
||||
if (!st->stop && (++st->steps > HELM_MAX_STEPS || hst_over_memory(st))) {
|
||||
st->stop = true;
|
||||
}
|
||||
return !st->stop;
|
||||
@@ -1418,7 +1421,7 @@ static bool hst_step(hst_t *st) {
|
||||
|
||||
/* n more bytes of output; false (and the render stopped) past the budget. */
|
||||
static bool hst_write(hst_t *st, size_t n) {
|
||||
if (st->stop || n > HELM_MAX_OUTPUT - st->written) {
|
||||
if (st->stop || n > HELM_MAX_OUTPUT - st->written || hst_over_memory(st)) {
|
||||
st->stop = true;
|
||||
return false;
|
||||
}
|
||||
@@ -1548,20 +1551,54 @@ static bool hv_poisoned(const hst_t *st, const hv_t *v) {
|
||||
if (!map) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < st->npoison; i++) {
|
||||
const hpoison_t *p = &st->poison[i];
|
||||
if (p->map == map) {
|
||||
return true;
|
||||
}
|
||||
if (p->deep && p->path && v->path) {
|
||||
size_t pl = strlen(p->path);
|
||||
if (pl == 0 || (strncmp(v->path, p->path, pl) == 0 &&
|
||||
(v->path[pl] == '\0' || v->path[pl] == '.' || v->path[pl] == '['))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
char key[CBM_SZ_32];
|
||||
snprintf(key, sizeof(key), "%p", map);
|
||||
if (st->poison_maps && cbm_ht_get(st->poison_maps, key)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
if (!st->poison_paths || !v->path) {
|
||||
return false;
|
||||
}
|
||||
/* a merged values path covers what is below it: each prefix of v's path
|
||||
* that ends at a key ("", "a", "a.b", "a.b[0]" ...; an escaped `\.` is no
|
||||
* end) is looked up */
|
||||
size_t pl = strlen(v->path);
|
||||
char small[CBM_SZ_256];
|
||||
char *buf = pl < sizeof(small) ? small : cbm_alloc(CBM_MEM_CLASS_OTHER, pl + SKIP_ONE);
|
||||
bool hit = false;
|
||||
for (size_t i = 0; buf && i <= pl && !hit; i++) {
|
||||
bool end = i == pl || ((v->path[i] == '.' || v->path[i] == '[') &&
|
||||
!(i > 0 && v->path[i - SKIP_ONE] == '\\'));
|
||||
if (!end) {
|
||||
continue;
|
||||
}
|
||||
memcpy(buf, v->path, i);
|
||||
buf[i] = '\0';
|
||||
hit = cbm_ht_get(st->poison_paths, buf) != NULL;
|
||||
}
|
||||
if (buf != small) {
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, buf);
|
||||
}
|
||||
return hit || !buf;
|
||||
}
|
||||
|
||||
/* Key `k` into set *set (made when NULL; the key copied into the arena). */
|
||||
static bool hst_set_add(hst_t *st, CBMHashTable **set, const char *k) {
|
||||
if (!*set) {
|
||||
*set = cbm_ht_create(0);
|
||||
}
|
||||
if (!*set) {
|
||||
return false;
|
||||
}
|
||||
if (cbm_ht_get(*set, k)) {
|
||||
return true;
|
||||
}
|
||||
const char *dup = cbm_arena_strdup(st->a, k);
|
||||
if (!dup) {
|
||||
return false;
|
||||
}
|
||||
cbm_ht_set(*set, dup, (void *)(uintptr_t)SKIP_ONE);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool hst_poison_at(hst_t *st, const hv_t *v, bool deep, int depth);
|
||||
@@ -1574,7 +1611,7 @@ static bool hst_poison(hst_t *st, const hv_t *v, bool deep) {
|
||||
}
|
||||
|
||||
static bool hst_poison_at(hst_t *st, const hv_t *v, bool deep, int depth) {
|
||||
if (depth > HELM_MAX_NESTING) {
|
||||
if (depth > HELM_MAX_NESTING || !hst_step(st)) {
|
||||
return false;
|
||||
}
|
||||
if (deep && v->k == HV_LIST) {
|
||||
@@ -1602,20 +1639,12 @@ static bool hst_poison_at(hst_t *st, const hv_t *v, bool deep, int depth) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (st->npoison == st->pcap) {
|
||||
int cap = st->pcap ? st->pcap * PAIR_LEN : CBM_SZ_16;
|
||||
hpoison_t *g = cbm_arena_alloc(st->a, (size_t)cap * sizeof(*g));
|
||||
if (!g) {
|
||||
return false;
|
||||
}
|
||||
if (st->npoison) {
|
||||
memcpy(g, st->poison, (size_t)st->npoison * sizeof(*g));
|
||||
}
|
||||
st->poison = g;
|
||||
st->pcap = cap;
|
||||
char key[CBM_SZ_32];
|
||||
snprintf(key, sizeof(key), "%p", map);
|
||||
if (!hst_set_add(st, &st->poison_maps, key)) {
|
||||
return false;
|
||||
}
|
||||
st->poison[st->npoison++] = (hpoison_t){map, v->path, deep};
|
||||
return true;
|
||||
return !(deep && v->path) || hst_set_add(st, &st->poison_paths, v->path);
|
||||
}
|
||||
|
||||
static bool hn_is(TSNode n, const char *t) {
|
||||
@@ -1743,13 +1772,24 @@ static void hst_capture_end(hst_t *st) {
|
||||
return;
|
||||
}
|
||||
cbm_helm_probe_t *p = &st->probes[st->capture];
|
||||
const char **img = cbm_arena_alloc(st->a, (size_t)(p->n + SKIP_ONE) * sizeof(*img));
|
||||
const char **keys = cbm_arena_alloc(st->a, (size_t)(p->n + SKIP_ONE) * sizeof(*keys));
|
||||
if (img && keys) {
|
||||
if (p->n) {
|
||||
memcpy(img, p->image, (size_t)p->n * sizeof(*img));
|
||||
memcpy(keys, p->keys, (size_t)p->n * sizeof(*keys));
|
||||
if (p->n == p->cap) {
|
||||
/* the captures of a probe grow by doubling: n captures cost O(n) */
|
||||
int cap = p->cap ? p->cap * PAIR_LEN : CBM_SZ_4;
|
||||
const char **gi = cbm_arena_alloc(st->a, (size_t)cap * sizeof(*gi));
|
||||
const char **gk = cbm_arena_alloc(st->a, (size_t)cap * sizeof(*gk));
|
||||
if (gi && gk) {
|
||||
if (p->n) {
|
||||
memcpy(gi, p->image, (size_t)p->n * sizeof(*gi));
|
||||
memcpy(gk, p->keys, (size_t)p->n * sizeof(*gk));
|
||||
}
|
||||
p->image = gi;
|
||||
p->keys = gk;
|
||||
p->cap = cap;
|
||||
}
|
||||
}
|
||||
const char **img = p->n < p->cap ? p->image : NULL;
|
||||
const char **keys = p->n < p->cap ? p->keys : NULL;
|
||||
if (img && keys) {
|
||||
img[p->n] = st->cap.unk || st->tainted
|
||||
? NULL
|
||||
: hst_scalar(st, st->cap.buf ? st->cap.buf : "", st->cap.n);
|
||||
@@ -1786,8 +1826,6 @@ static void hst_capture_end(hst_t *st) {
|
||||
if (!whole || (json && !keys[p->n])) {
|
||||
img[p->n] = NULL; /* a mark not written in full: no image, never one unmarked */
|
||||
}
|
||||
p->image = img;
|
||||
p->keys = keys;
|
||||
p->n++;
|
||||
}
|
||||
st->capture = -SKIP_ONE;
|
||||
@@ -2453,14 +2491,25 @@ static bool he_deep_value(const hfresh_t *f, const char *src, TSNode x, int dept
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Variable definition `def` (`$x := ...`) into f when what it makes is new. */
|
||||
static void he_fresh_note(hst_t *st, hfresh_t *f, const char *src, TSNode def) {
|
||||
TSNode var = hn_field(def, "variable");
|
||||
TSNode val = hn_field(def, "value");
|
||||
hfresh_lvl_t lvl = ts_node_is_null(var) ? HF_NONE
|
||||
: he_deep_value(f, src, val, 0) ? HF_DEEP
|
||||
: he_fresh_value(f, src, val);
|
||||
if (lvl == HF_NONE) {
|
||||
/* Variable var is `lvl` new from here on (HF_NONE too: what it held before
|
||||
* no longer counts -- the latest entry of a name is the one looked up). */
|
||||
/* Has the scan seen variable var defined or assigned (an entry in f)? */
|
||||
static bool he_fresh_seen(const hfresh_t *f, const char *src, TSNode var) {
|
||||
size_t vl = 0;
|
||||
const char *vs = he_var_text(src, var, &vl);
|
||||
for (int i = 0; i < f->n; i++) {
|
||||
if (f->len[i] == vl && memcmp(f->name[i], vs, vl) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Note in f that variable var is now lvl-new. A first HF_NONE entry is kept
|
||||
* too: it hides the running frame's value of var from the scan. */
|
||||
static void he_fresh_put(hst_t *st, hfresh_t *f, const char *src, TSNode var, hfresh_lvl_t lvl) {
|
||||
if (ts_node_is_null(var) ||
|
||||
(lvl == HF_NONE && he_fresh_seen(f, src, var) && he_fresh_var(f, src, var) == HF_NONE)) {
|
||||
return;
|
||||
}
|
||||
if (f->n == f->cap) {
|
||||
@@ -2483,6 +2532,77 @@ static void he_fresh_note(hst_t *st, hfresh_t *f, const char *src, TSNode def) {
|
||||
f->n++;
|
||||
}
|
||||
|
||||
/* Is expression x deep-new for the scan (f) -- or, a variable the scan did
|
||||
* not see made, with fr (a part of the running template), a value of fr
|
||||
* that is own? */
|
||||
static bool he_value_owned(hst_t *st, hframe_t *fr, const hfresh_t *f, const char *src, TSNode x) {
|
||||
if (he_deep_value(f, src, x, 0)) {
|
||||
return true;
|
||||
}
|
||||
if (!fr || !hn_is(x, "variable") || he_fresh_seen(f, src, x)) {
|
||||
return false;
|
||||
}
|
||||
size_t l = 0;
|
||||
const char *name = hn_var_name(fr, x, &l);
|
||||
hvar_t *v = hst_var(st, fr, name, l);
|
||||
return v && v->v.own;
|
||||
}
|
||||
|
||||
/* Is call node c a set / unset / merge of variable var whose every other
|
||||
* argument is own (the variable stays as new as it is)? */
|
||||
static bool he_self_change(hst_t *st, hframe_t *fr, const hfresh_t *f, const char *src, TSNode c,
|
||||
TSNode var) {
|
||||
size_t fl = 0;
|
||||
const char *fs = hn_is(c, "function_call") ? he_fn_text(src, c, &fl) : NULL;
|
||||
TSNode args = fs && he_mutator(fs, fl) ? hn_field(c, "arguments") : (TSNode){0};
|
||||
uint32_t na = ts_node_is_null(args) ? 0 : ts_node_named_child_count(args);
|
||||
TSNode first = na ? ts_node_named_child(args, 0) : (TSNode){0};
|
||||
if (!hn_is(first, "variable")) {
|
||||
return false;
|
||||
}
|
||||
size_t al = 0;
|
||||
size_t bl = 0;
|
||||
const char *as = he_var_text(src, first, &al);
|
||||
const char *bs = he_var_text(src, var, &bl);
|
||||
if (al != bl || memcmp(as, bs, al) != 0) {
|
||||
return false;
|
||||
}
|
||||
for (uint32_t i = SKIP_ONE; i < na; i++) {
|
||||
if (!he_value_owned(st, fr, f, src, ts_node_named_child(args, i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* A definition (`$x := v`) or an assignment (`$x = v`) into f: how new $x is
|
||||
* now -- what v is, or, for `$x = set $x ...` of own values, what $x was. */
|
||||
static void he_fresh_note(hst_t *st, hframe_t *fr, hfresh_t *f, const char *src, TSNode def) {
|
||||
TSNode var = hn_field(def, "variable");
|
||||
TSNode val = hn_field(def, "value");
|
||||
if (ts_node_is_null(var)) {
|
||||
return;
|
||||
}
|
||||
hfresh_lvl_t lvl = he_deep_value(f, src, val, 0) ? HF_DEEP : he_fresh_value(f, src, val);
|
||||
if (lvl == HF_NONE && hn_is(def, "assignment") && he_self_change(st, fr, f, src, val, var)) {
|
||||
lvl = he_fresh_var(f, src, var);
|
||||
}
|
||||
he_fresh_put(st, f, src, var, lvl);
|
||||
}
|
||||
|
||||
/* Are the values set / merge call c puts into its target own (see
|
||||
* he_value_owned)? */
|
||||
static bool he_puts_deep(hst_t *st, hframe_t *fr, const hfresh_t *f, const char *src, TSNode c) {
|
||||
TSNode args = hn_field(c, "arguments");
|
||||
uint32_t na = ts_node_is_null(args) ? 0 : ts_node_named_child_count(args);
|
||||
for (uint32_t i = SKIP_ONE; i < na; i++) {
|
||||
if (!he_value_owned(st, fr, f, src, ts_node_named_child(args, i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !hn_is(ts_node_parent(c), "chained_pipeline"); /* a piped value: not known here */
|
||||
}
|
||||
|
||||
/* Does expression x read `.` (a field of it)? In a with / range body `.` is
|
||||
* not the frame's. */
|
||||
static bool he_reads_dot(TSNode x) {
|
||||
@@ -2505,12 +2625,36 @@ static bool he_reads_dot(TSNode x) {
|
||||
return found;
|
||||
}
|
||||
|
||||
/* Does expression x name a variable the scan (f) saw defined or assigned --
|
||||
* one whose value in the running frame is not the one x would read? */
|
||||
static bool he_names_rebound(const hfresh_t *f, const char *src, TSNode x) {
|
||||
TSTreeCursor cur = ts_tree_cursor_new(x);
|
||||
bool found = false;
|
||||
for (bool more = true; more && !found;) {
|
||||
TSNode c = ts_tree_cursor_current_node(&cur);
|
||||
found = hn_is(c, "variable") && he_fresh_seen(f, src, c);
|
||||
if (found || ts_tree_cursor_goto_first_child(&cur)) {
|
||||
continue;
|
||||
}
|
||||
while (!ts_tree_cursor_goto_next_sibling(&cur)) {
|
||||
if (!ts_tree_cursor_goto_parent(&cur)) {
|
||||
more = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ts_tree_cursor_delete(&cur);
|
||||
return found;
|
||||
}
|
||||
|
||||
/* What a change of the value of expression x (in frame fr; NULL: not
|
||||
* running) would reach: false when it reaches nothing another part holds --
|
||||
* x is own, or a known map, now poisoned (`deep`: a merge). A value not known
|
||||
* here: true. */
|
||||
static bool he_change_reaches(hst_t *st, hframe_t *fr, TSNode x, bool dot_known, bool deep) {
|
||||
if (!fr || ts_node_is_null(x) || (!dot_known && he_reads_dot(x))) {
|
||||
* here -- or x naming a variable the part (f) redefined: true. */
|
||||
static bool he_change_reaches(hst_t *st, hframe_t *fr, const hfresh_t *f, const char *src, TSNode x,
|
||||
bool dot_known, bool deep) {
|
||||
if (!fr || ts_node_is_null(x) || (!dot_known && he_reads_dot(x)) ||
|
||||
he_names_rebound(f, src, x)) {
|
||||
return true;
|
||||
}
|
||||
/* read without recording: the part is not run, its reads are no capture's */
|
||||
@@ -2523,21 +2667,27 @@ static bool he_change_reaches(hst_t *st, hframe_t *fr, TSNode x, bool dot_known,
|
||||
return !poisoned && !v.own;
|
||||
}
|
||||
|
||||
static void he_revoke_own(hst_t *st, hframe_t *fr, TSNode x);
|
||||
|
||||
/* May running node n (of text src) change a map another part holds -- a set /
|
||||
* unset / merge whose target is `.`, `$`, a field path, or a variable n did
|
||||
* not make itself from a new map (`$d := dict ...`), in n or in a define it
|
||||
* includes (whose changes reach only its argument)? A call's result as the
|
||||
* target is a new value: no. With fr (n is a part of the running template
|
||||
* not run), a target or include argument that is a value of fr is looked at:
|
||||
* own, or a known map (then poisoned): no. An include of a computed name is a
|
||||
* template file by path, another template's work, which no render models.
|
||||
* The text a tpl renders is not looked into. */
|
||||
* unset / merge whose target is not a value n made new itself (`$d := dict`,
|
||||
* `set (dict) ...`; a merge needs it deep-new), in n or in a define it
|
||||
* includes (whose changes reach only its argument)? With fr (n is a part of
|
||||
* the running template not run), a target or include argument -- `.`, a
|
||||
* field, a call's result such as `index .Values "image"`, a variable of fr --
|
||||
* is looked at: own, or a known map (then poisoned): no. A change that puts a
|
||||
* value another part may hold into a variable makes it no longer new / own.
|
||||
* An include of a computed name is a template file by path, another
|
||||
* template's work, which no render models. The text a tpl renders is not
|
||||
* looked into. `made` (NULL: none) is what the parts before n in its body
|
||||
* made new or redefined; n's own are added to it. */
|
||||
static bool he_may_mutate(hst_t *st, const char *src, TSNode n, int depth, hframe_t *fr,
|
||||
bool dot_known) {
|
||||
bool dot_known, hfresh_t *made) {
|
||||
if (depth > HELM_MAX_NESTING) {
|
||||
return true;
|
||||
}
|
||||
hfresh_t fresh = {0}; /* variables n makes from a new map */
|
||||
hfresh_t own = {0}; /* variables n makes from a new map */
|
||||
hfresh_t *fresh = made ? made : &own;
|
||||
TSTreeCursor cur = ts_tree_cursor_new(n);
|
||||
bool found = false;
|
||||
bool more = true;
|
||||
@@ -2546,20 +2696,31 @@ static bool he_may_mutate(hst_t *st, const char *src, TSNode n, int depth, hfram
|
||||
const char *t = ts_node_type(c);
|
||||
size_t fl = 0;
|
||||
const char *fs = NULL;
|
||||
if (strcmp(t, "variable_definition") == 0) {
|
||||
he_fresh_note(st, &fresh, src, c);
|
||||
if (strcmp(t, "variable_definition") == 0 || strcmp(t, "assignment") == 0) {
|
||||
he_fresh_note(st, fr, fresh, src, c);
|
||||
} else if (strcmp(t, "function_call") == 0 && (fs = he_fn_text(src, c, &fl)) != NULL &&
|
||||
he_mutator(fs, fl)) {
|
||||
TSNode args = hn_field(c, "arguments");
|
||||
TSNode target = ts_node_is_null(args) ? (TSNode){0} : ts_node_named_child(args, 0);
|
||||
bool deep = fs[0] == 'm'; /* a merge reaches the maps within its target too */
|
||||
if (hn_is(target, "variable")) {
|
||||
hfresh_lvl_t lvl = he_fresh_var(&fresh, src, target);
|
||||
bool spared = lvl == HF_DEEP || (lvl == HF_SHALLOW && !deep);
|
||||
found = !spared && he_change_reaches(st, fr, target, dot_known, deep);
|
||||
} else if (ts_node_is_null(target) || hn_is(target, "dot") || hn_is(target, "field") ||
|
||||
hn_is(target, "selector_expression")) {
|
||||
found = he_change_reaches(st, fr, target, dot_known, deep);
|
||||
bool puts_deep = he_puts_deep(st, fr, fresh, src, c);
|
||||
/* how new the target is: a variable n made, or a new value itself
|
||||
* (`set (dict) ...`); anything else -- `.`, a field, a call's
|
||||
* result (`index .Values "image"`) -- is looked at in fr */
|
||||
hfresh_lvl_t lvl = hn_is(target, "variable") ? he_fresh_var(fresh, src, target)
|
||||
: ts_node_is_null(target) ? HF_NONE
|
||||
: he_deep_value(fresh, src, target, 0)
|
||||
? HF_DEEP
|
||||
: he_fresh_value(fresh, src, target);
|
||||
bool spared = lvl == HF_DEEP || (lvl == HF_SHALLOW && !deep);
|
||||
found = !spared && he_change_reaches(st, fr, fresh, src, target, dot_known, deep);
|
||||
if (!puts_deep && hn_is(target, "variable")) {
|
||||
/* it now holds a value another part may hold */
|
||||
if (lvl == HF_DEEP) {
|
||||
he_fresh_put(st, fresh, src, target, HF_SHALLOW);
|
||||
} else if (fr && lvl == HF_NONE) {
|
||||
he_revoke_own(st, fr, target);
|
||||
}
|
||||
}
|
||||
} else if (strcmp(t, "function_call") == 0 || strcmp(t, "template_action") == 0 ||
|
||||
strcmp(t, "block_action") == 0) {
|
||||
@@ -2572,8 +2733,8 @@ static bool he_may_mutate(hst_t *st, const char *src, TSNode n, int depth, hfram
|
||||
? (ts_node_is_null(args) ? (TSNode){0}
|
||||
: ts_node_named_child(args, SKIP_ONE))
|
||||
: hn_field(c, "argument");
|
||||
found = !he_deep_value(&fresh, src, arg, 0) &&
|
||||
he_change_reaches(st, fr, arg, dot_known, true);
|
||||
found = !he_deep_value(fresh, src, arg, 0) &&
|
||||
he_change_reaches(st, fr, fresh, src, arg, dot_known, true);
|
||||
}
|
||||
} else if (strcmp(t, "identifier") == 0 && (fs = he_fn_text(src, c, &fl)) != NULL &&
|
||||
he_mutator(fs, fl) && hn_is(ts_node_parent(c), "chained_pipeline")) {
|
||||
@@ -2594,9 +2755,12 @@ static bool he_may_mutate(hst_t *st, const char *src, TSNode n, int depth, hfram
|
||||
return found || st->stop;
|
||||
}
|
||||
|
||||
/* Memoized per define (a define reached again while it is scanned: no). */
|
||||
/* Memoized per define. A define reached again while it is being scanned
|
||||
* counts as no for now; a no that leaned on such a one is not kept (STALE:
|
||||
* scanned again when next asked), so defines including each other never fix
|
||||
* a wrong no -- a yes is final. */
|
||||
static bool he_define_may_mutate(hst_t *st, const char *name, int depth) {
|
||||
enum { HM_NO = 1, HM_YES = 2 };
|
||||
enum { HM_NO = 1, HM_YES = 2, HM_BUSY = 3, HM_STALE = 4 };
|
||||
const hd_def_t *d = hd_find(st->defs, name);
|
||||
if (!d) {
|
||||
return false; /* a define not found is an error: nothing of it runs */
|
||||
@@ -2604,36 +2768,54 @@ static bool he_define_may_mutate(hst_t *st, const char *name, int depth) {
|
||||
if (!st->mutates) {
|
||||
st->mutates = cbm_ht_create(0);
|
||||
}
|
||||
void *memo = st->mutates ? cbm_ht_get(st->mutates, name) : NULL;
|
||||
if (memo) {
|
||||
return memo == (void *)(uintptr_t)HM_YES;
|
||||
uintptr_t memo = st->mutates ? (uintptr_t)cbm_ht_get(st->mutates, name) : 0;
|
||||
if (memo == HM_YES || memo == HM_NO) {
|
||||
return memo == HM_YES;
|
||||
}
|
||||
const char *key = cbm_arena_strdup(st->a, name);
|
||||
if (st->mutates && key) {
|
||||
cbm_ht_set(st->mutates, key, (void *)(uintptr_t)HM_NO);
|
||||
if (memo == HM_BUSY) {
|
||||
st->busy_hits++;
|
||||
return false;
|
||||
}
|
||||
bool m = he_may_mutate(st, d->file->src, d->node, depth, NULL, false);
|
||||
const char *key = memo ? cbm_ht_get_key(st->mutates, name) : cbm_arena_strdup(st->a, name);
|
||||
if (st->mutates && key) {
|
||||
cbm_ht_set(st->mutates, key, (void *)(uintptr_t)(m ? HM_YES : HM_NO));
|
||||
cbm_ht_set(st->mutates, key, (void *)(uintptr_t)HM_BUSY);
|
||||
}
|
||||
long hits = st->busy_hits;
|
||||
bool m = he_may_mutate(st, d->file->src, d->node, depth, NULL, false, NULL);
|
||||
bool leaned = st->busy_hits > hits; /* on a define still being scanned */
|
||||
if (st->mutates && key) {
|
||||
uintptr_t res = m ? HM_YES : leaned ? HM_STALE : HM_NO;
|
||||
cbm_ht_set(st->mutates, key, (void *)res);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
static hv_t he_apply_fn(hst_t *st, hframe_t *fr, const char *fn, hv_t *a, int n);
|
||||
|
||||
/* A define not run would change its argument v (or a map within it): a known
|
||||
* one is poisoned whole; one not known that is not own spoils what follows. */
|
||||
static void he_spoil_arg(hst_t *st, const hv_t *v) {
|
||||
bool known = hv_is_map(v) || hv_is_list(v);
|
||||
if (!(known && hst_poison(st, v, true)) && !v->own &&
|
||||
(v->k == HV_UNK || hv_is_map(v) || hv_is_list(v))) {
|
||||
st->tainted = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* Is the result of `fn` over a own (see hv_t): a part dug / got / indexed /
|
||||
* taken out of an own value; a dict / list / default / coalesce / ternary
|
||||
* whose every map or list (and every value not known) is own? */
|
||||
static bool he_own_result(const char *fn, const hv_t *a, int n) {
|
||||
* taken out of an own value no change has touched (a poisoned map may hold
|
||||
* another's now); a dict / list / default / coalesce / ternary whose every map
|
||||
* or list (and every value not known) is own? */
|
||||
static bool he_own_result(const hst_t *st, const char *fn, const hv_t *a, int n) {
|
||||
if (n < SKIP_ONE) {
|
||||
return strcmp(fn, "dict") == 0 || strcmp(fn, "list") == 0; /* empty and new */
|
||||
}
|
||||
if (strcmp(fn, "dig") == 0) {
|
||||
return a[n - SKIP_ONE].own;
|
||||
return a[n - SKIP_ONE].own && !hv_poisoned(st, &a[n - SKIP_ONE]);
|
||||
}
|
||||
if (strcmp(fn, "get") == 0 || strcmp(fn, "index") == 0 || strcmp(fn, "first") == 0 ||
|
||||
strcmp(fn, "last") == 0) {
|
||||
return a[0].own;
|
||||
return a[0].own && !hv_poisoned(st, &a[0]);
|
||||
}
|
||||
if (strcmp(fn, "dict") == 0 || strcmp(fn, "list") == 0 || strcmp(fn, "default") == 0 ||
|
||||
strcmp(fn, "coalesce") == 0 || strcmp(fn, "ternary") == 0) {
|
||||
@@ -2654,28 +2836,73 @@ static hv_t he_apply(hst_t *st, hframe_t *fr, const char *fn, hv_t *a, int n) {
|
||||
return HV_UNKNOWN_OWN; /* fromYaml / deepCopy: a new value, known or not */
|
||||
}
|
||||
hv_t r = he_apply_fn(st, fr, fn, a, n);
|
||||
r.own = r.own || he_own_result(fn, a, n);
|
||||
r.own = r.own || he_own_result(st, fn, a, n);
|
||||
return r;
|
||||
}
|
||||
|
||||
/* The variable expression x reads its value out of (`$v`, `$v.a`, `(index $v
|
||||
* "a")`, `get $v "a"`, `dig "a" $v`); a null node when there is none. */
|
||||
static TSNode he_root_var(const hframe_t *fr, TSNode x) {
|
||||
for (int depth = 0; depth < HELM_MAX_NESTING && !ts_node_is_null(x); depth++) {
|
||||
if (hn_is(x, "variable")) {
|
||||
return x;
|
||||
}
|
||||
if (hn_is(x, "parenthesized_pipeline") && ts_node_named_child_count(x) == SKIP_ONE) {
|
||||
x = ts_node_named_child(x, 0);
|
||||
} else if (hn_is(x, "selector_expression")) {
|
||||
x = hn_field(x, "operand");
|
||||
} else if (hn_is(x, "function_call")) {
|
||||
size_t fl = 0;
|
||||
const char *fs = he_fn_text(fr->src, x, &fl);
|
||||
TSNode args = hn_field(x, "arguments");
|
||||
uint32_t na = ts_node_is_null(args) ? 0 : ts_node_named_child_count(args);
|
||||
bool last = fs && fl == strlen("dig") && memcmp(fs, "dig", fl) == 0;
|
||||
bool first = fs && ((fl == strlen("get") && memcmp(fs, "get", fl) == 0) ||
|
||||
(fl == strlen("index") && memcmp(fs, "index", fl) == 0) ||
|
||||
(fl == strlen("first") && memcmp(fs, "first", fl) == 0) ||
|
||||
(fl == strlen("last") && memcmp(fs, "last", fl) == 0));
|
||||
if (!na || (!first && !last)) {
|
||||
break;
|
||||
}
|
||||
x = ts_node_named_child(args, last ? na - SKIP_ONE : 0);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (TSNode){0};
|
||||
}
|
||||
|
||||
/* A change put a value another part may hold into what expression x reads:
|
||||
* the variable it reads it out of is own no more. */
|
||||
static void he_revoke_own(hst_t *st, hframe_t *fr, TSNode x) {
|
||||
TSNode var = he_root_var(fr, x);
|
||||
if (ts_node_is_null(var)) {
|
||||
return;
|
||||
}
|
||||
size_t l = 0;
|
||||
const char *name = hn_var_name(fr, var, &l);
|
||||
hvar_t *v = hst_var(st, fr, name, l);
|
||||
if (v) {
|
||||
v->v.own = false;
|
||||
}
|
||||
}
|
||||
|
||||
static hv_t he_apply_fn(hst_t *st, hframe_t *fr, const char *fn, hv_t *a, int n) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (a[i].k == HV_UNK) {
|
||||
/* an include of a known define not run: a change it would have
|
||||
* made to its argument is missed, unless that one is own (one of a
|
||||
* name not known -- a template file by path -- is another
|
||||
* template's work, which no render models) */
|
||||
if (strcmp(fn, "include") == 0 && a[0].k == HV_STR && n == PAIR_LEN && !a[1].own) {
|
||||
if (a[i].k == HV_UNK || hv_poisoned(st, &a[i])) {
|
||||
/* an include of a known define not run (an argument UNKNOWN or
|
||||
* poisoned): a change it would make to its argument is missed --
|
||||
* a known argument is poisoned whole, one not known and not own
|
||||
* spoils what follows (one of a name not known -- a template file
|
||||
* by path -- is another template's work, which no render models) */
|
||||
if (strcmp(fn, "include") == 0 && a[0].k == HV_STR && n == PAIR_LEN) {
|
||||
const char *dn = cbm_arena_strndup(st->a, a[0].s, a[0].sn);
|
||||
if (!dn || he_define_may_mutate(st, dn, st->nesting)) {
|
||||
st->tainted = true;
|
||||
he_spoil_arg(st, &a[1]);
|
||||
}
|
||||
}
|
||||
return HV_UNKNOWN;
|
||||
}
|
||||
if (hv_poisoned(st, &a[i])) {
|
||||
return HV_UNKNOWN;
|
||||
}
|
||||
}
|
||||
if (strcmp(fn, "include") == 0 && n == PAIR_LEN && a[0].k == HV_STR) {
|
||||
hsink_t out = {0};
|
||||
@@ -3031,8 +3258,8 @@ static hv_t he_call(hst_t *st, hframe_t *fr, TSNode call, const hv_t *piped) {
|
||||
if (he_mutator(fn, fl)) {
|
||||
/* set / unset / merge change a map in place -- not modelled: what it
|
||||
* holds is UNKNOWN from here on (after a merge, every map within it).
|
||||
* A target not known that may be one another part holds -- a variable,
|
||||
* `.`, a field path; a call's result is a new value -- spoils every
|
||||
* A target not known that is not own -- whatever expression gives it:
|
||||
* index / get / first return a map another part holds -- spoils every
|
||||
* image after it. */
|
||||
TSNode first = na ? ts_node_named_child(args, 0) : (TSNode){0};
|
||||
hv_t target = na ? he_eval(st, fr, first) : piped ? *piped : HV_UNKNOWN;
|
||||
@@ -3042,13 +3269,14 @@ static hv_t he_call(hst_t *st, hframe_t *fr, TSNode call, const hv_t *piped) {
|
||||
hv_t v = i < na ? he_eval(st, fr, ts_node_named_child(args, i)) : *piped;
|
||||
put_own = put_own && (v.own || (v.k != HV_UNK && !hv_is_map(&v) && !hv_is_list(&v)));
|
||||
}
|
||||
bool held = ts_node_is_null(first) || hn_is(first, "variable") || hn_is(first, "dot") ||
|
||||
hn_is(first, "field") || hn_is(first, "selector_expression");
|
||||
/* a known map is poisoned even when own: what it holds is not known now */
|
||||
bool poisoned = hst_poison(st, &target, fn[0] == 'm');
|
||||
if (!poisoned && !target.own && (held || hv_is_map(&target))) {
|
||||
if (!poisoned && !target.own) {
|
||||
st->tainted = true;
|
||||
}
|
||||
if (!put_own && !ts_node_is_null(first)) {
|
||||
he_revoke_own(st, fr, first);
|
||||
}
|
||||
/* the changed map is what set / merge return */
|
||||
return target.own && put_own ? HV_UNKNOWN_OWN : HV_UNKNOWN;
|
||||
}
|
||||
@@ -3235,21 +3463,6 @@ static void he_items(hst_t *st, hframe_t *fr, TSNode parent, const char *field)
|
||||
}
|
||||
}
|
||||
|
||||
/* Is expression x deep-new (f: what the scan saw made new), or a variable of
|
||||
* frame fr whose value is own? */
|
||||
static bool he_part_own(hst_t *st, hframe_t *fr, const hfresh_t *f, TSNode x) {
|
||||
if (he_deep_value(f, fr->src, x, 0)) {
|
||||
return true;
|
||||
}
|
||||
if (!hn_is(x, "variable")) {
|
||||
return false;
|
||||
}
|
||||
size_t l = 0;
|
||||
const char *name = hn_var_name(fr, x, &l);
|
||||
hvar_t *v = hst_var(st, fr, name, l);
|
||||
return v && v->v.own;
|
||||
}
|
||||
|
||||
/* Does assigning expression `value` to variable var keep it own (when it is):
|
||||
* a deep-new value, or a set / merge of var itself with own values
|
||||
* (`$dst = ... | fromYaml | merge $dst`)? */
|
||||
@@ -3287,36 +3500,40 @@ static bool he_assign_owns(hst_t *st, hframe_t *fr, const hfresh_t *f, TSNode va
|
||||
return false;
|
||||
}
|
||||
for (uint32_t i = SKIP_ONE; i < na; i++) {
|
||||
if (!he_part_own(st, fr, f, ts_node_named_child(args, i))) {
|
||||
if (!he_value_owned(st, fr, f, src, ts_node_named_child(args, i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return ts_node_is_null(piped) || he_part_own(st, fr, f, piped);
|
||||
return ts_node_is_null(piped) || he_value_owned(st, fr, f, src, piped);
|
||||
}
|
||||
|
||||
/* How a part not run would leave the loop around it. */
|
||||
enum { HL_NONE = 0, HL_CONTINUE = 1, HL_BREAK = 2 };
|
||||
|
||||
/* The variables of the frames around n that n assigns (`$x = ...`) become
|
||||
* UNKNOWN (an own one stays own when what n gives it keeps it so); true when
|
||||
* n would leave the loop around it -- a break / continue not inside a range
|
||||
* of n's own (`in_range`: n is a range's body). */
|
||||
static bool he_scan_skipped(hst_t *st, hframe_t *fr, TSNode n, bool in_range) {
|
||||
* UNKNOWN (an own one stays own when what n gives it keeps it so); returns
|
||||
* how n would leave the loop around it (HL_*) -- by a break / continue not
|
||||
* inside a range of n's own (`in_range`: n is a range's body). `fresh` is
|
||||
* what the parts before n in its body made new; n's own are added to it. */
|
||||
static int he_scan_skipped(hst_t *st, hframe_t *fr, TSNode n, bool in_range, hfresh_t *fresh) {
|
||||
TSTreeCursor cur = ts_tree_cursor_new(n);
|
||||
hfresh_t fresh = {0}; /* variables n makes new */
|
||||
bool leave = false;
|
||||
int leave = HL_NONE;
|
||||
bool more = true;
|
||||
while (more && hst_step(st)) {
|
||||
TSNode c = ts_tree_cursor_current_node(&cur);
|
||||
const char *t = ts_node_type(c);
|
||||
if (strcmp(t, "variable_definition") == 0) {
|
||||
he_fresh_note(st, &fresh, fr->src, c);
|
||||
he_fresh_note(st, fr, fresh, fr->src, c);
|
||||
} else if (strcmp(t, "assignment") == 0) {
|
||||
TSNode var = hn_field(c, "variable");
|
||||
size_t l = 0;
|
||||
const char *name = ts_node_is_null(var) ? NULL : hn_var_name(fr, var, &l);
|
||||
hvar_t *v = name ? hst_var(st, fr, name, l) : NULL;
|
||||
if (v) {
|
||||
bool keeps = v->v.own && he_assign_owns(st, fr, &fresh, var, hn_field(c, "value"));
|
||||
bool keeps = v->v.own && he_assign_owns(st, fr, fresh, var, hn_field(c, "value"));
|
||||
v->v = keeps ? HV_UNKNOWN_OWN : HV_UNKNOWN;
|
||||
}
|
||||
he_fresh_note(st, fr, fresh, fr->src, c);
|
||||
} else if (strcmp(t, "break_action") == 0 || strcmp(t, "continue_action") == 0) {
|
||||
/* a range around it inside n (n itself included) is the one it leaves */
|
||||
bool own = in_range || hn_is(n, "range_action");
|
||||
@@ -3324,7 +3541,9 @@ static bool he_scan_skipped(hst_t *st, hframe_t *fr, TSNode n, bool in_range) {
|
||||
p = ts_node_parent(p);
|
||||
own = hn_is(p, "range_action");
|
||||
}
|
||||
leave = leave || !own;
|
||||
if (!own) {
|
||||
leave |= t[0] == 'b' ? HL_BREAK : HL_CONTINUE;
|
||||
}
|
||||
}
|
||||
if (ts_tree_cursor_goto_first_child(&cur)) {
|
||||
continue;
|
||||
@@ -3340,32 +3559,70 @@ static bool he_scan_skipped(hst_t *st, hframe_t *fr, TSNode n, bool in_range) {
|
||||
return leave;
|
||||
}
|
||||
|
||||
/* The bodies of if / with / range n, not run because its condition or
|
||||
* collection is UNKNOWN (that one was run): their output, the variables they
|
||||
* would assign and a change they would make are UNKNOWN; a loop they would
|
||||
* leave is left now (its later iterations are not known). */
|
||||
static void he_skipped(hst_t *st, hframe_t *fr, TSNode n) {
|
||||
hst_out_unknown(st, fr);
|
||||
bool range = hn_is(n, "range_action");
|
||||
bool with = hn_is(n, "with_action");
|
||||
bool leave = false;
|
||||
/* The children of n but its condition / collection (what was run), each:
|
||||
* the variables it would assign and the changes it would make spoiled (see
|
||||
* he_scan_skipped / he_may_mutate); returns how it would leave the loop. */
|
||||
static int he_spoil_parts(hst_t *st, hframe_t *fr, TSNode n, const char *only, bool in_range,
|
||||
bool dot_known) {
|
||||
int leave = HL_NONE;
|
||||
bool mutates = false;
|
||||
/* what a branch's parts made new so far: a later part of it sees it */
|
||||
hfresh_t scanned = {0};
|
||||
hfresh_t made = {0};
|
||||
const char *branch = NULL;
|
||||
uint32_t cc = ts_node_child_count(n);
|
||||
for (uint32_t i = 0; i < cc; i++) {
|
||||
TSNode c = ts_node_child(n, i);
|
||||
const char *f = ts_node_field_name_for_child(n, i);
|
||||
if (f && strcmp(f, "condition") == 0) {
|
||||
branch = NULL; /* an else if: a new branch */
|
||||
}
|
||||
if (!ts_node_is_named(c) || hn_is(c, "range_variable_definition") ||
|
||||
(f && (strcmp(f, "condition") == 0 || strcmp(f, "range") == 0))) {
|
||||
(f && (strcmp(f, "condition") == 0 || strcmp(f, "range") == 0)) ||
|
||||
(only && !(f && strcmp(f, only) == 0))) {
|
||||
continue;
|
||||
}
|
||||
leave = he_scan_skipped(st, fr, c, range) || leave;
|
||||
mutates = mutates || he_may_mutate(st, fr->src, c, st->nesting, fr, !range && !with);
|
||||
if (f && (!branch || strcmp(f, branch) != 0)) {
|
||||
branch = f;
|
||||
scanned = (hfresh_t){0};
|
||||
made = (hfresh_t){0};
|
||||
}
|
||||
leave |= he_scan_skipped(st, fr, c, in_range, &scanned);
|
||||
mutates = mutates || he_may_mutate(st, fr->src, c, st->nesting, fr, dot_known, &made);
|
||||
}
|
||||
if (mutates) {
|
||||
st->tainted = true;
|
||||
}
|
||||
if (leave) {
|
||||
return leave;
|
||||
}
|
||||
|
||||
/* The bodies of if / with / range n, not run because its condition or
|
||||
* collection is UNKNOWN (that one was run): their output, the variables they
|
||||
* would assign and a change they would make are UNKNOWN. A break / continue
|
||||
* in them may or may not leave the loop around: what the rest of that loop's
|
||||
* body would assign or change is UNKNOWN too, and a break leaves it now (its
|
||||
* later iterations are not known), a continue this iteration. */
|
||||
static void he_skipped(hst_t *st, hframe_t *fr, TSNode n) {
|
||||
hst_out_unknown(st, fr);
|
||||
bool range = hn_is(n, "range_action");
|
||||
bool with = hn_is(n, "with_action");
|
||||
int leave = he_spoil_parts(st, fr, n, NULL, range, !range && !with);
|
||||
if (leave == HL_NONE) {
|
||||
return;
|
||||
}
|
||||
TSNode loop = ts_node_parent(n);
|
||||
for (int depth = 0;
|
||||
!ts_node_is_null(loop) && !hn_is(loop, "range_action") && depth < HELM_MAX_NESTING;
|
||||
depth++) {
|
||||
loop = ts_node_parent(loop);
|
||||
}
|
||||
if (hn_is(loop, "range_action")) {
|
||||
(void)he_spoil_parts(st, fr, loop, "body", true, false);
|
||||
}
|
||||
if (leave & HL_BREAK) {
|
||||
st->brk = true;
|
||||
} else {
|
||||
st->cont = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3508,9 +3765,9 @@ static void he_template(hst_t *st, hframe_t *fr, TSNode n) {
|
||||
if (a.k == HV_UNK || hv_poisoned(st, &a)) {
|
||||
hst_out_unknown(st, fr);
|
||||
/* the define not run: a change it would make to its argument is
|
||||
* missed, unless that one is own */
|
||||
if (!a.own && he_define_may_mutate(st, name, st->nesting)) {
|
||||
st->tainted = true;
|
||||
* missed (see he_spoil_arg) */
|
||||
if (he_define_may_mutate(st, name, st->nesting)) {
|
||||
he_spoil_arg(st, &a);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -3612,6 +3869,7 @@ bool cbm_helm_render(CBMArena *a, const char *rel, const char *src, size_t len,
|
||||
probes[i].n = 0;
|
||||
probes[i].image = NULL;
|
||||
probes[i].keys = NULL;
|
||||
probes[i].cap = 0;
|
||||
}
|
||||
if (!src || nprobes == 0) {
|
||||
return true;
|
||||
@@ -3622,7 +3880,8 @@ bool cbm_helm_render(CBMArena *a, const char *rel, const char *src, size_t len,
|
||||
.top_len = len,
|
||||
.probes = probes,
|
||||
.nprobes = nprobes,
|
||||
.capture = -SKIP_ONE};
|
||||
.capture = -SKIP_ONE,
|
||||
.arena0 = cbm_arena_total(a)};
|
||||
st.scratch = yyjson_mut_doc_new(cbm_json_writer_allocator());
|
||||
st.parser = ts_parser_new();
|
||||
if (!st.scratch || !st.parser ||
|
||||
@@ -3679,6 +3938,12 @@ bool cbm_helm_render(CBMArena *a, const char *rel, const char *src, size_t len,
|
||||
if (st.mutates) {
|
||||
cbm_ht_free(st.mutates);
|
||||
}
|
||||
if (st.poison_maps) {
|
||||
cbm_ht_free(st.poison_maps);
|
||||
}
|
||||
if (st.poison_paths) {
|
||||
cbm_ht_free(st.poison_paths);
|
||||
}
|
||||
if (st.stop) {
|
||||
/* Helm would not finish this template: none of its images is known,
|
||||
* also those captured before the budget ran out */
|
||||
@@ -3689,7 +3954,9 @@ bool cbm_helm_render(CBMArena *a, const char *rel, const char *src, size_t len,
|
||||
}
|
||||
}
|
||||
cbm_log_warn("helm.render.budget", "file", rel ? rel : "", "reason",
|
||||
st.steps > HELM_MAX_STEPS ? "steps" : "output");
|
||||
st.steps > HELM_MAX_STEPS ? "steps"
|
||||
: st.written >= HELM_MAX_OUTPUT ? "output"
|
||||
: "memory");
|
||||
}
|
||||
return !st.stop;
|
||||
}
|
||||
@@ -68,6 +68,7 @@ void cbm_helm_defines_free(cbm_helm_defines_t *d);
|
||||
typedef struct {
|
||||
int line; /* 1-based line of the key in the template */
|
||||
int n; /* captures */
|
||||
int cap; /* room in image / keys (the render's own) */
|
||||
const char **image; /* the rendered scalar, YAML quoting removed; NULL: UNKNOWN */
|
||||
const char **keys; /* JSON array of the values paths it read; NULL: none */
|
||||
} cbm_helm_probe_t;
|
||||
|
||||
+132
-16
@@ -3904,6 +3904,7 @@ typedef struct {
|
||||
k8s_himg_t *v;
|
||||
int n;
|
||||
int cap;
|
||||
CBMHashTable *seen; /* "src:img" of each v (keys cbm_alloc'd): one look-up a push */
|
||||
} k8s_himgs_t;
|
||||
|
||||
typedef struct {
|
||||
@@ -3919,6 +3920,9 @@ 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. */
|
||||
@@ -3948,16 +3952,29 @@ static char *k8s_repo_read(cbm_pipeline_ctx_t *ctx, const char *rel, int *len) {
|
||||
}
|
||||
char path[CBM_SZ_4K];
|
||||
int n = snprintf(path, sizeof(path), "%s/%s", ctx->repo_path, rel);
|
||||
return n > 0 && (size_t)n < sizeof(path) ? k8s_read_file(path, len) : NULL;
|
||||
if (n <= 0 || (size_t)n >= sizeof(path)) {
|
||||
return NULL;
|
||||
}
|
||||
char *buf = k8s_read_file(path, len);
|
||||
cbm_path_info_t info;
|
||||
if (!buf && cbm_path_info_utf8(path, &info) == CBM_PATH_INFO_OK && info.is_regular &&
|
||||
!info.is_symlink && info.size == 0) {
|
||||
/* an empty file is read: empty text (padded like every read) */
|
||||
enum { CBM_TS_EMPTY_PAD = 16 };
|
||||
buf = cbm_calloc(CBM_MEM_CLASS_OTHER, CBM_TS_EMPTY_PAD);
|
||||
*len = 0;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Is repository path `rel` there at all -- a file, a symlink, anything (a
|
||||
* link is reported, never followed)? */
|
||||
/* Is repository path `rel` there, or can that not be told (a link is
|
||||
* reported, never followed; only a path proven absent is not there)? */
|
||||
static bool k8s_repo_has(cbm_pipeline_ctx_t *ctx, const char *rel) {
|
||||
char path[CBM_SZ_4K];
|
||||
int n = snprintf(path, sizeof(path), "%s/%s", ctx->repo_path, rel);
|
||||
cbm_path_info_t info;
|
||||
return n > 0 && (size_t)n < sizeof(path) && cbm_path_info_utf8(path, &info) == 0;
|
||||
return n > 0 && (size_t)n < sizeof(path) &&
|
||||
cbm_path_info_utf8(path, &info) != CBM_PATH_INFO_ABSENT;
|
||||
}
|
||||
|
||||
/* A file of `dir` (one of `names`), parsed as values; *found when one exists.
|
||||
@@ -4232,14 +4249,38 @@ static const char *k8s_helm_overridden(k8s_helm_t *h, const char *keys, yyjson_m
|
||||
}
|
||||
|
||||
static void k8s_himg_push(k8s_himgs_t *l, k8s_himg_t x) {
|
||||
for (int i = 0; i < l->n; i++) {
|
||||
if (l->v[i].src == x.src && l->v[i].img == x.img) {
|
||||
return; /* the first (in file order) of the same Image stands for it */
|
||||
}
|
||||
char key[CBM_SZ_64];
|
||||
snprintf(key, sizeof(key), "%lld:%lld", (long long)x.src, (long long)x.img);
|
||||
if (!l->seen) {
|
||||
l->seen = cbm_ht_create(0);
|
||||
}
|
||||
if (k8s_grow((void **)&l->v, &l->cap, l->n, sizeof(*l->v))) {
|
||||
l->v[l->n++] = x;
|
||||
if (!l->seen || cbm_ht_get(l->seen, key)) {
|
||||
return; /* the first (in file order) of the same Image stands for it */
|
||||
}
|
||||
size_t kl = strlen(key) + SKIP_ONE;
|
||||
char *k = cbm_alloc(CBM_MEM_CLASS_OTHER, kl);
|
||||
if (!k || !k8s_grow((void **)&l->v, &l->cap, l->n, sizeof(*l->v))) {
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, k);
|
||||
return;
|
||||
}
|
||||
memcpy(k, key, kl);
|
||||
cbm_ht_set(l->seen, k, (void *)(uintptr_t)SKIP_ONE);
|
||||
l->v[l->n++] = x;
|
||||
}
|
||||
|
||||
static void k8s_himg_key_free(const char *key, void *value, void *ud) {
|
||||
(void)value;
|
||||
(void)ud;
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, (void *)key);
|
||||
}
|
||||
|
||||
static void k8s_himgs_free(k8s_himgs_t *l) {
|
||||
if (l->seen) {
|
||||
cbm_ht_foreach(l->seen, k8s_himg_key_free, NULL);
|
||||
cbm_ht_free(l->seen);
|
||||
}
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, l->v);
|
||||
*l = (k8s_himgs_t){0};
|
||||
}
|
||||
|
||||
/* Does `src` USE Image `img` already by a literal reference? */
|
||||
@@ -4313,8 +4354,68 @@ 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. */
|
||||
* overlay changed: rendered there, absent from the defaults' images. An image
|
||||
* reading a set_sensitive name (whose value is not read) is not known. */
|
||||
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,
|
||||
@@ -4328,7 +4429,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) {
|
||||
if (same || !ovr || from <= 0 || k8s_helm_sensitive_keys(h, x->keys, prefix)) {
|
||||
continue;
|
||||
}
|
||||
k8s_himg_t e = {from, x->img, x->ref, NULL, ovr};
|
||||
@@ -4593,7 +4694,7 @@ static void k8s_helm_release_local(cbm_pipeline_ctx_t *ctx, k8s_bundles_t *b, k8
|
||||
yyjson_mut_val *globals = yyjson_mut_obj_get(rv, "global");
|
||||
k8s_helm_emit_overrides(ctx, h, src, via, &h->dflt, &got, overlay, globals,
|
||||
path && path[0] ? path : NULL, emitted);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, got.v);
|
||||
k8s_himgs_free(&got);
|
||||
}
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, srcs);
|
||||
from = to;
|
||||
@@ -4647,7 +4748,9 @@ static void k8s_helm_convention(cbm_pipeline_ctx_t *ctx, k8s_helm_t *h, int64_t
|
||||
: "",
|
||||
dig[0] ? dig : tag)
|
||||
: NULL;
|
||||
int64_t img = ref && k8s_helm_has_host(ref) && cbm_image_ref_valid(ref)
|
||||
/* 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)
|
||||
? cbm_pipeline_image_node(ctx, ref)
|
||||
: 0;
|
||||
int before = emitted->n;
|
||||
@@ -4699,7 +4802,7 @@ static void k8s_helm_release(cbm_pipeline_ctx_t *ctx, k8s_bundles_t *b, k8s_helm
|
||||
cbm_ht_get(b->charts, dir)) {
|
||||
k8s_helm_release_local(ctx, b, h, l, src, via, dir, rv, &emitted);
|
||||
}
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, emitted.v);
|
||||
k8s_himgs_free(&emitted);
|
||||
}
|
||||
|
||||
/* A node's document (lines start_line..end_line of its file) as values. */
|
||||
@@ -4792,6 +4895,16 @@ static yyjson_mut_val *k8s_helm_kustomize_values(cbm_pipeline_ctx_t *ctx, k8s_he
|
||||
char *src = kust->file_path ? k8s_repo_read(ctx, kust->file_path, &len) : NULL;
|
||||
yyjson_mut_val *doc = src ? cbm_helm_values_parse(h->doc, src, (size_t)len) : NULL;
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, src);
|
||||
/* kustomize runs the kustomization's own transformers over the inflated
|
||||
* chart: images / patches / replacements may change what it deploys */
|
||||
static const char *const TRANSFORMS[] = {
|
||||
"images", "patches", "patchesJson6902", "patchesStrategicMerge",
|
||||
"replacements", "transformers", "components", NULL};
|
||||
for (int i = 0; TRANSFORMS[i]; i++) {
|
||||
if (k8s_helm_gives(doc, TRANSFORMS[i])) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
yyjson_mut_val *charts = yyjson_mut_is_obj(doc) ? yyjson_mut_obj_get(doc, "helmCharts") : NULL;
|
||||
yyjson_mut_val *inline_vals = NULL;
|
||||
size_t idx, max;
|
||||
@@ -4874,9 +4987,12 @@ 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;
|
||||
@@ -4905,7 +5021,7 @@ static void k8s_helm_free(k8s_helm_t *h) {
|
||||
yyjson_mut_doc_free(h->doc);
|
||||
}
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, h->t);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, h->dflt.v);
|
||||
k8s_himgs_free(&h->dflt);
|
||||
cbm_arena_destroy(&h->a);
|
||||
if (h->edges > 0) {
|
||||
cbm_log_info("pass.k8s.helm_images", "edges", itoa_k8s(h->edges));
|
||||
|
||||
+109
-14
@@ -523,15 +523,27 @@ static yyjson_mut_val *tf_json(const tf_t *t, yyjson_mut_doc *doc, TSNode n) {
|
||||
static char *tf_read(const char *path, size_t *len);
|
||||
|
||||
/* A repository file's text (cbm_alloc; capped and padded like every file
|
||||
* read), NULL when unreadable -- or when it is no file discovery took in: a
|
||||
* symlink, or a path through one, is never followed out of the repository. */
|
||||
* read; an empty file is empty text), NULL when unreadable -- or when it is
|
||||
* no file discovery took in: a symlink, or a path through one, is never
|
||||
* followed out of the repository. */
|
||||
static char *tf_read_repo(const tf_t *t, const char *path, size_t *len) {
|
||||
if (!cbm_pipeline_file_node(t->ctx->gbuf, t->ctx->project_name, path)) {
|
||||
return NULL;
|
||||
}
|
||||
char full[CBM_SZ_4K];
|
||||
int n = snprintf(full, sizeof(full), "%s/%s", t->ctx->repo_path, path);
|
||||
return n > 0 && (size_t)n < sizeof(full) ? tf_read(full, len) : NULL;
|
||||
if (n <= 0 || (size_t)n >= sizeof(full)) {
|
||||
return NULL;
|
||||
}
|
||||
char *buf = tf_read(full, len);
|
||||
cbm_path_info_t info;
|
||||
if (!buf && cbm_path_info_utf8(full, &info) == CBM_PATH_INFO_OK && info.is_regular &&
|
||||
!info.is_symlink && info.size == 0) {
|
||||
enum { TF_EMPTY_PAD = 16 };
|
||||
buf = cbm_calloc(CBM_MEM_CLASS_OTHER, TF_EMPTY_PAD);
|
||||
*len = 0;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* A `values = [...]` item as values: a YAML string / heredoc, file(<path>)
|
||||
@@ -633,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"` / `dynamic "set_list"` -- entries made by a
|
||||
* for_each, never literal? */
|
||||
/* Is block `a` a `dynamic "set"` / `"set_list"` / `"set_sensitive"` --
|
||||
* entries (and names) 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);
|
||||
@@ -649,17 +661,72 @@ static bool tf_dynamic_set(const tf_t *t, TSNode a) {
|
||||
uint32_t lb = ts_node_start_byte(label);
|
||||
size_t ll = ts_node_end_byte(label) - lb;
|
||||
return (ll == strlen("\"set\"") && strncmp(t->src + lb, "\"set\"", ll) == 0) ||
|
||||
(ll == strlen("\"set_list\"") && strncmp(t->src + lb, "\"set_list\"", ll) == 0);
|
||||
(ll == strlen("\"set_list\"") && strncmp(t->src + lb, "\"set_list\"", ll) == 0) ||
|
||||
(ll == strlen("\"set_sensitive\"") &&
|
||||
strncmp(t->src + lb, "\"set_sensitive\"", ll) == 0);
|
||||
}
|
||||
|
||||
/* Is node n an identifier spelling `word`? */
|
||||
static bool tf_word(const tf_t *t, TSNode n, const char *word) {
|
||||
if (ts_node_is_null(n)) {
|
||||
return false;
|
||||
}
|
||||
uint32_t b = ts_node_start_byte(n);
|
||||
size_t l = ts_node_end_byte(n) - b;
|
||||
return l == strlen(word) && strncmp(t->src + b, word, l) == 0;
|
||||
}
|
||||
|
||||
/* The 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, or is made by a `dynamic` block. set_sensitive is
|
||||
* never read. */
|
||||
static char *tf_release_values(const tf_t *t, TSNode body, const char ***files, int *nfiles) {
|
||||
* 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;
|
||||
yyjson_mut_doc *doc = yyjson_mut_doc_new(cbm_json_writer_allocator());
|
||||
yyjson_mut_val *root = doc ? yyjson_mut_obj(doc) : NULL;
|
||||
bool ok = root != NULL;
|
||||
yyjson_mut_val *names = doc ? yyjson_mut_arr(doc) : NULL;
|
||||
bool ok = root != NULL && names != 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);
|
||||
@@ -669,12 +736,30 @@ static char *tf_release_values(const tf_t *t, TSNode body, const char ***files,
|
||||
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 */
|
||||
* 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_dynamic_set(t, a) || (tf_type(a, "block") && tf_word(t, key, "postrender"))) {
|
||||
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");
|
||||
@@ -706,6 +791,15 @@ 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);
|
||||
@@ -727,8 +821,9 @@ static void tf_release_push(tf_t *t, int64_t src, int64_t chart, TSNode body) {
|
||||
}
|
||||
const char **files = NULL;
|
||||
int nfiles = 0;
|
||||
ctx->tf_releases[ctx->tf_release_n++] =
|
||||
(cbm_tf_release_t){src, chart, tf_release_values(t, body, &files, &nfiles)};
|
||||
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};
|
||||
/* 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,9 +385,10 @@ 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 */
|
||||
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 */
|
||||
} cbm_tf_release_t;
|
||||
|
||||
/* Shared context passed to each pass function.
|
||||
|
||||
@@ -4871,6 +4871,78 @@ 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. */
|
||||
TEST(probe_helm_release_terraform_sensitive_and_postrender) {
|
||||
static const GpfFile files[] = {
|
||||
GPF_APP_CHART,
|
||||
{"tf/main.tf",
|
||||
"resource \"helm_release\" \"tagged\" {\n name = \"tagged\"\n"
|
||||
" chart = \"../charts/app\"\n set {\n name = \"image.repository\"\n"
|
||||
" value = \"quay.io/acme/app\"\n }\n set_sensitive {\n name = \"image.tag\"\n"
|
||||
" value = var.tag\n }\n}\n"
|
||||
"resource \"helm_release\" \"pw\" {\n name = \"pw\"\n"
|
||||
" chart = \"../charts/app\"\n set {\n name = \"image.repository\"\n"
|
||||
" value = \"quay.io/acme/pw\"\n }\n set_sensitive {\n name = \"db.password\"\n"
|
||||
" value = var.pw\n }\n}\n"
|
||||
"resource \"helm_release\" \"post\" {\n name = \"post\"\n"
|
||||
" chart = \"../charts/app\"\n set {\n name = \"image.tag\"\n"
|
||||
" value = \"3.3\"\n }\n postrender {\n binary_path = \"./kustomize.sh\"\n }\n}\n"}};
|
||||
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"
|
||||
"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");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A kustomization that also transforms what it inflates (images:, patches)
|
||||
* has no release image of its own: the transformer may change it. */
|
||||
TEST(probe_helm_release_kustomize_transformers) {
|
||||
static const GpfFile files[] = {
|
||||
{"kz/kustomization.yaml",
|
||||
"apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\nhelmCharts:\n"
|
||||
" - name: app\n releaseName: kapp\n valuesInline:\n image:\n"
|
||||
" tag: \"1.5\"\nimages:\n - name: ghcr.io/acme/app\n newTag: \"2.0\"\n"},
|
||||
{"kz/charts/app/Chart.yaml", "apiVersion: v2\nname: app\nversion: 1.0.0\n"},
|
||||
{"kz/charts/app/values.yaml", "image:\n repository: ghcr.io/acme/app\n tag: \"1.0\"\n"},
|
||||
{"kz/charts/app/templates/deploy.yaml",
|
||||
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: app\nspec:\n template:\n"
|
||||
" spec:\n containers:\n - name: app\n"
|
||||
" image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"}};
|
||||
if (!gpf_expect_image_edges(files, 4, GPF_APP_DEFAULTS)) {
|
||||
FAIL("a kustomization's own transformers make its release images unknown");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* An empty values.yaml is values {} (Helm's), not values not read: the
|
||||
* subchart under it still renders its image. */
|
||||
TEST(probe_helm_empty_values_file_renders) {
|
||||
static const GpfFile files[] = {
|
||||
{"charts/app/Chart.yaml",
|
||||
"apiVersion: v2\nname: app\nversion: 1.0.0\nappVersion: \"1.0\"\n"},
|
||||
{"charts/app/values.yaml", ""},
|
||||
{"charts/app/templates/deploy.yaml",
|
||||
"apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: app\nspec:\n template:\n"
|
||||
" spec:\n containers:\n - name: app\n"
|
||||
" image: \"ghcr.io/acme/{{ .Chart.Name }}:{{ .Chart.AppVersion }}\"\n"}};
|
||||
static const char want[] =
|
||||
"Chart:app -> Image:ghcr.io/acme/app DEPLOYS image=- via=chart ref=image\n"
|
||||
"Resource:Deployment/app -> Image:ghcr.io/acme/app USAGE image=ghcr.io/acme/app:1.0 "
|
||||
"ref=image";
|
||||
if (!gpf_expect_image_edges(files, 3, want)) {
|
||||
FAIL("an empty values.yaml renders with {}");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A subchart a dependency aliases renders with .Chart.Name its alias (Helm's
|
||||
* getAliasDependency), so an image naming the chart names the alias. */
|
||||
TEST(probe_helm_aliased_subchart_chart_name) {
|
||||
@@ -4919,8 +4991,10 @@ TEST(probe_helm_list_path_mark_on_chart_edge) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
/* gpf_image_edges with symlinks laid in after the files: links[i] names a
|
||||
* repository path and the absolute or relative target it points at. */
|
||||
* repository path and the absolute or relative target it points at (POSIX
|
||||
* symlinks: the tests using it skip on Windows). */
|
||||
static char *gpf_image_edges_linked(const GpfFile *files, int nfiles, const GpfFile *links,
|
||||
int nlinks) {
|
||||
GpfProj lp;
|
||||
@@ -4975,6 +5049,7 @@ static char *gpf_image_edges_linked(const GpfFile *files, int nfiles, const GpfF
|
||||
gpf_cleanup(&lp, store);
|
||||
return out;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* A values file a chart, a kustomization or a Terraform file() names by path
|
||||
* is read only when it is a file of the repository: a symlink out of it --
|
||||
@@ -4982,6 +5057,9 @@ static char *gpf_image_edges_linked(const GpfFile *files, int nfiles, const GpfF
|
||||
* file is such a link renders nothing (its values are unknown, not empty);
|
||||
* the releases reading one have no image of their own. */
|
||||
TEST(probe_helm_values_symlink_not_followed) {
|
||||
#ifdef _WIN32
|
||||
SKIP_PLATFORM("POSIX symlink contract");
|
||||
#else
|
||||
char outside[256];
|
||||
snprintf(outside, sizeof(outside), "/tmp/cbm_gpf_out_XXXXXX");
|
||||
ASSERT_NOT_NULL(cbm_mkdtemp(outside));
|
||||
@@ -5024,11 +5102,15 @@ TEST(probe_helm_values_symlink_not_followed) {
|
||||
FAIL("no values read through a symlink out of the repository");
|
||||
}
|
||||
PASS();
|
||||
#endif
|
||||
}
|
||||
|
||||
/* A Terraform helm_release reading its values with file() of a symlink out
|
||||
* of the repository has no image of its own: the link is never followed. */
|
||||
TEST(probe_helm_terraform_values_symlink_not_followed) {
|
||||
#ifdef _WIN32
|
||||
SKIP_PLATFORM("POSIX symlink contract");
|
||||
#else
|
||||
char outside[256];
|
||||
snprintf(outside, sizeof(outside), "/tmp/cbm_gpf_out_XXXXXX");
|
||||
ASSERT_NOT_NULL(cbm_mkdtemp(outside));
|
||||
@@ -5057,6 +5139,7 @@ TEST(probe_helm_terraform_values_symlink_not_followed) {
|
||||
FAIL("no Terraform values read through a symlink out of the repository");
|
||||
}
|
||||
PASS();
|
||||
#endif
|
||||
}
|
||||
|
||||
/* A build argument holding a secret -- by its name (*_TOKEN) or its value (a
|
||||
@@ -6929,6 +7012,9 @@ SUITE(grammar_probe_f) {
|
||||
RUN_TEST(probe_helm_release_flux_unread_sources);
|
||||
RUN_TEST(probe_helm_release_kustomize_unread_sources);
|
||||
RUN_TEST(probe_helm_release_terraform_dynamic_set);
|
||||
RUN_TEST(probe_helm_release_terraform_sensitive_and_postrender);
|
||||
RUN_TEST(probe_helm_release_kustomize_transformers);
|
||||
RUN_TEST(probe_helm_empty_values_file_renders);
|
||||
RUN_TEST(probe_helm_aliased_subchart_chart_name);
|
||||
RUN_TEST(probe_helm_list_path_mark_on_chart_edge);
|
||||
RUN_TEST(probe_helm_values_symlink_not_followed);
|
||||
|
||||
@@ -4559,6 +4559,191 @@ TEST(helm_change_of_a_new_value_spoils_nothing) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* An image line inside a range: n captures of one probe cost memory linear
|
||||
* in n (the capture arrays grow by doubling), not a copy of every earlier
|
||||
* capture each time. */
|
||||
TEST(helm_render_captures_grow_linearly) {
|
||||
enum { ITER = 5000 };
|
||||
static const char tpl[] = "{{- range 5000 }}\n"
|
||||
"image: \"ghcr.io/acme/app:1\"\n"
|
||||
"{{ end }}\n";
|
||||
CBMArena a;
|
||||
cbm_arena_init_lazy(&a, CBM_SZ_4K);
|
||||
cbm_helm_probe_t probe = {.line = 2};
|
||||
size_t before = cbm_arena_total(&a);
|
||||
cbm_helm_render(&a, "t.yaml", tpl, strlen(tpl), NULL, NULL, NULL, &probe, 1);
|
||||
size_t used = cbm_arena_total(&a) - before;
|
||||
int n = probe.n;
|
||||
cbm_arena_destroy(&a);
|
||||
ASSERT_EQ(n, ITER);
|
||||
ASSERT_TRUE(used < (size_t)CBM_SZ_16 * CBM_SZ_1K * CBM_SZ_1K); /* far below 8*n^2 bytes */
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Memory a render takes -- here a 20,000-item values list copied by each of
|
||||
* 200 `first` calls -- counts against its budget: past 256 MiB it stops and
|
||||
* the template's captures are UNKNOWN. */
|
||||
TEST(helm_render_memory_budget_ends_unknown) {
|
||||
enum { ITEMS = 20000 };
|
||||
size_t cap = (size_t)ITEMS * 4 + CBM_SZ_64;
|
||||
char *vals = malloc(cap);
|
||||
ASSERT_NOT_NULL(vals);
|
||||
size_t o = (size_t)snprintf(vals, cap, "tag: \"1.0\"\nbig: [");
|
||||
for (int i = 0; i < ITEMS; i++) {
|
||||
o += (size_t)snprintf(vals + o, cap - o, "%s1", i ? "," : "");
|
||||
}
|
||||
snprintf(vals + o, cap - o, "]\n");
|
||||
static const char tpl[] = "image: \"ghcr.io/acme/app:{{ .Values.tag }}\"\n"
|
||||
"{{- range 200 }}{{ $x := first $.Values.big }}{{ end }}\n";
|
||||
char *got = helm_capture(vals, tpl, NULL, 1);
|
||||
free(vals);
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A set skipped by an UNKNOWN condition whose target is a call's result --
|
||||
* `index .Values "image"`, the idiom for a dashed key -- may change the map
|
||||
* it names: what the image reads of it is UNKNOWN. */
|
||||
TEST(helm_skipped_set_of_a_call_result) {
|
||||
static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\n";
|
||||
static const char tpl[] =
|
||||
"{{- if semverCompare \">=1.29-0\" .Capabilities.KubeVersion.Version }}"
|
||||
"{{- $_ := set (index .Values \"image\") \"tag\" \"2.0\" }}{{- end }}\n"
|
||||
"image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n";
|
||||
char *got = helm_capture(vals, tpl, NULL, 2);
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A new map given a map another part holds is new no more: a change made
|
||||
* through it (`set $img ...` of `index $d "image"`) reaches .Values. */
|
||||
TEST(helm_own_map_given_a_shared_one) {
|
||||
static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\n";
|
||||
static const char tpl[] = "{{- $d := dict -}}{{- $_ := set $d \"image\" .Values.image -}}\n"
|
||||
"{{- $img := index $d \"image\" -}}{{- $_ := set $img \"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();
|
||||
}
|
||||
|
||||
/* A set whose target is an UNKNOWN call result (index by .Release.Name) may
|
||||
* change any map of the values: every image after it is UNKNOWN. */
|
||||
TEST(helm_set_of_an_unknown_call_result) {
|
||||
static const char vals[] =
|
||||
"apps: {web: {repository: ghcr.io/acme/web, tag: \"1.0\"}}\n";
|
||||
static const char tpl[] = "{{- $_ := set (index .Values.apps .Release.Name) \"tag\" \"2.0\" }}\n"
|
||||
"image: \"{{ .Values.apps.web.repository }}:{{ .Values.apps.web.tag }}\"\n";
|
||||
char *got = helm_capture(vals, tpl, NULL, 2);
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A break under an UNKNOWN condition: whether the rest of the loop ran is
|
||||
* unknown, so what it assigns ($tag = .) is UNKNOWN after the loop. */
|
||||
TEST(helm_unknown_break_spoils_the_rest_of_the_loop) {
|
||||
static const char vals[] = "versions: [\"1.0\", \"2.0\"]\n";
|
||||
static const char tpl[] =
|
||||
"{{- $tag := \"0.9\" }}{{- range .Values.versions }}\n"
|
||||
"{{- if semverCompare \">=1.29-0\" $.Capabilities.KubeVersion.Version }}{{ break }}"
|
||||
"{{ end }}\n"
|
||||
"{{- $tag = . }}{{- end }}\n"
|
||||
"image: \"ghcr.io/acme/app:{{ $tag }}\"\n";
|
||||
char *got = helm_capture(vals, tpl, NULL, 4);
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
TEST(helm_skipped_set_after_redefinition) {
|
||||
static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\n";
|
||||
static const char redefined[] =
|
||||
"{{- $cfg := dict }}{{- if .Capabilities.APIVersions }}"
|
||||
"{{- $cfg := .Values.image }}{{- $_ := set $cfg \"tag\" \"2.0\" }}{{- end }}\n"
|
||||
"image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n";
|
||||
static const char assigned[] =
|
||||
"{{- $cfg := dict }}{{- if .Capabilities.APIVersions }}"
|
||||
"{{- $cfg = .Values.image }}{{- $_ := set $cfg \"tag\" \"2.0\" }}{{- end }}\n"
|
||||
"image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n";
|
||||
static const char kept[] = "{{- $cfg := dict }}{{- if .Capabilities.APIVersions }}"
|
||||
"{{- $_ := set $cfg \"tag\" \"2.0\" }}{{- end }}\n"
|
||||
"image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n";
|
||||
char *got = helm_capture(vals, redefined, NULL, 2);
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
got = helm_capture(vals, assigned, NULL, 2);
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
got = helm_capture(vals, kept, NULL, 2);
|
||||
ASSERT_STR_EQ(got, "ghcr.io/acme/app:1.0");
|
||||
free(got);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Many maps poisoned (a new dict changed each iteration) keep every later
|
||||
* read correct: a values read after them is known. */
|
||||
TEST(helm_many_changed_maps_keep_reads_right) {
|
||||
static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\n";
|
||||
static const char tpl[] = "{{- range 20000 }}{{ $c := dict }}{{ $_ := set $c \"k\" \"v\" }}"
|
||||
"{{ end }}\n"
|
||||
"image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n";
|
||||
char *got = helm_capture(vals, tpl, NULL, 2);
|
||||
ASSERT_STR_EQ(got, "ghcr.io/acme/app:1.0");
|
||||
free(got);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Defines that include each other: A changes its argument and includes B,
|
||||
* B includes A. Whether B may change its argument is not settled by A's
|
||||
* scan still running -- an UNKNOWN include of B with .Values.image spoils
|
||||
* what reads it. */
|
||||
TEST(helm_define_memo_under_mutual_recursion) {
|
||||
static const char defs_src[] =
|
||||
"{{- define \"A\" -}}{{- include \"B\" . -}}{{- $_ := set . \"tag\" \"2.0\" -}}{{- end -}}\n"
|
||||
"{{- define \"B\" -}}{{- if false }}{{ include \"A\" . }}{{ end -}}{{- end -}}\n";
|
||||
cbm_helm_defines_t *d = cbm_helm_defines_new();
|
||||
ASSERT_NOT_NULL(d);
|
||||
cbm_helm_defines_add(d, "templates/_ab.tpl", defs_src, strlen(defs_src));
|
||||
static const char vals[] =
|
||||
"image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\nother: {tag: \"x\"}\n";
|
||||
static const char tpl[] =
|
||||
"{{- if .Capabilities.APIVersions }}{{ include \"A\" .Values.other }}{{ end }}\n"
|
||||
"{{- if .Capabilities.APIVersions }}{{ include \"B\" .Values.image }}{{ end }}\n"
|
||||
"image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n";
|
||||
char *got = helm_capture(vals, tpl, d, 3);
|
||||
cbm_helm_defines_free(d);
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* An include not run because its argument is poisoned may still change maps
|
||||
* within it: a handle taken before ($sub) reads UNKNOWN after. */
|
||||
TEST(helm_include_with_poisoned_argument) {
|
||||
static const char defs_src[] =
|
||||
"{{- define \"mut\" -}}{{- $_ := set .sub \"tag\" \"2.0\" -}}{{- end -}}\n";
|
||||
cbm_helm_defines_t *d = cbm_helm_defines_new();
|
||||
ASSERT_NOT_NULL(d);
|
||||
cbm_helm_defines_add(d, "templates/_mut.tpl", defs_src, strlen(defs_src));
|
||||
static const char vals[] = "image: {sub: {repository: ghcr.io/acme/app, tag: \"1.0\"}}\n";
|
||||
static const char tpl[] = "{{- $sub := .Values.image.sub -}}\n"
|
||||
"{{- $_ := set .Values.image \"x\" \"y\" -}}\n"
|
||||
"{{- include \"mut\" .Values.image -}}\n"
|
||||
"image: \"{{ $sub.repository }}:{{ $sub.tag }}\"\n";
|
||||
char *got = helm_capture(vals, tpl, d, 4);
|
||||
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. */
|
||||
@@ -24297,6 +24482,16 @@ SUITE(pipeline_semantic_manifest_repro) {
|
||||
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_render_captures_grow_linearly);
|
||||
RUN_TEST(helm_render_memory_budget_ends_unknown);
|
||||
RUN_TEST(helm_skipped_set_of_a_call_result);
|
||||
RUN_TEST(helm_own_map_given_a_shared_one);
|
||||
RUN_TEST(helm_set_of_an_unknown_call_result);
|
||||
RUN_TEST(helm_unknown_break_spoils_the_rest_of_the_loop);
|
||||
RUN_TEST(helm_skipped_set_after_redefinition);
|
||||
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(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