mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(deploy): taint Helm changes of and/or, later conditions, sortAlpha, parse errors
Review round 4 of #2677 (user decisions: R4-1..R4-4, R4-6, R4-7, R4-9;
R4-5 measured and left out). Every case ends in an UNKNOWN image, never a
wrong one.
Changes not run exactly (helm_template.c):
- A deep merge into a values map is not exact when a change was made at or
below its path (a set storing a map there): every values map a set or a
merge changes keeps its path, and such a merge leaves the images after it
UNKNOWN.
- and / or at an operand whose truth is not known -- UNKNOWN, or a map a
change made unknown -- leave the images after them UNKNOWN when an
operand after it may change maps.
- In an if / with an UNKNOWN condition skips, the else-if / else-with
conditions after it are looked at too: a change there taints, an
assignment there makes its variable UNKNOWN.
- sortAlpha sorts a []string in place: it counts as a change (a list is
never poisoned, so it taints). No other Sprig list function changes its
argument in place.
- A template, a define body or a scanned template file the grammar does not
parse may hide a change: its images are UNKNOWN.
Work (helm_template.c):
- A capture's keys leave the key set one by one; the set is no longer
cleared by walking its whole capacity after each capture.
- Template files are found by a hash of their path, and an include of a
name that is no define is answered once per render.
Known limit, documented (README): each template file renders on its own,
while Helm renders a chart's files with one shared .Values. Running them in
Helm's order is the planned follow-up; the coarse rule across files cost 83
more correct images on the 20 infra corpora and was left out.
On the 20 infra corpora: Image edges identical to round 4 (0 lost, 0
added), no budget trip.
Tests: R4-1..R4-4 and R4-9 fail on 3401def5 by their assertions (each sub
case too); R4-6 and R4-7 are correctness controls, their cost measured
outside the suite.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
3401def5ba
commit
5cdbe9b0b6
4 files changed
+312
-47
No files matched your search
@@ -333,6 +333,7 @@ The install script placed beside the binary is **reported, not deleted**: uninst
|
||||
- `INFRA_MAPS`: a Kubernetes Service to each workload whose pod labels carry every `key: value` of the Service's selector, within one namespace
|
||||
- `CONNECTS_TO`: a Kubernetes workload or compose service to the Service its containers' environment addresses (`PRODUCT_CATALOG_SERVICE_ADDR: productcatalogservice:3550`, a URL's host, `<svc>.<ns>.svc...`, a bare name on an `*_ADDR` / `*_HOST` / `*_URL` / ... key; compose: the project's `.env` interpolated): a Kubernetes Service of the same bundle and namespace, or a compose service of the same file. The edge keeps `via: env`, `key` and `container`, never the value
|
||||
- `USAGE` with `ref: image`: a Kubernetes workload (any pod template: containers, initContainers, CronJobs, custom resources) or a docker compose service to the `Image` it runs
|
||||
- Helm templates are rendered for their images: with the chart's values (the `Chart` `DEPLOYS` them, `default_of`: the values keys read) and with each release's (a Flux HelmRelease, a kustomize `helmCharts` entry, a Terraform `helm_release`: `override_of`). An image the render cannot know is left out, never guessed: after a change of values it does not run exactly, a `tpl` or include it cannot look into, a release value it cannot read, or a work budget it runs past. Known limit: each template file renders on its own, while Helm renders a chart's files with one shared `.Values` (a subchart's is its parent's `.Values.<alias>`), so a chart that changes `.Values` in one file and reads it in another can still show a wrong image
|
||||
- `DEPLOYS`: what puts manifests, kustomizations or charts on a cluster, to what it puts there: a Flux Kustomization or Argo Application to the kustomizations / manifests / chart of its path, a Flux HelmRelease to its chart (`spec.chart` or `chartRef` → OCIRepository), a shell command or Make recipe (`kubectl apply -f/-k`, `kustomize build | kubectl apply`, `helm install/upgrade/template`), a Go installer loading a chart with the Helm SDK, a Terraform `helm_release`, a Terraform `kubernetes_*` resource the object it declares (a `Resource` like a manifest's, `ecosystem: terraform`, when its name is a literal), a kustomization inflating a chart (`helmCharts`; its values file an `IMPORTS`). A chart outside the repository is an external `Chart`, keyed by its publisher when its repository URL is stated (`__chart__<repository>/<name>`, the same node in every project, the URL in `repository`), else one per project and name; the edge says `via` (`flux`, `argo`, `helmrelease`, `kubectl`, `kustomize`, `helm`, `helm-sdk`, `terraform`, `helmcharts`, ...)
|
||||
- `DEPENDS_ON`: a Helm chart to the charts it declares (the local chart for a resolvable `file://` repository, otherwise an external `Chart`; the edge keeps `repository`), and a `go.mod` / `requirements.txt` file to an external `Package`; a Dockerfile to the `Image`s it builds on (`via: from`, `copy-from`); an `Image` to the Dockerfile it is built from (`via: build`; `tool`: `skaffold`, `compose`, `docker-build` for `docker` / `podman` / `nerdctl` / `buildah` build commands in scripts, Make and CI); a compose service to the services its `depends_on` / `links` name
|
||||
- `CONFIGURES`: code reading an environment variable to the shared `EnvVar` of that name (`strategy: env_access`); a Kubernetes container, compose service or Dockerfile (`ENV` of the image) setting one to that container's own `EnvVar` (`strategy: env_set`, with `source`: `value`, `configMapKeyRef`, `secretKeyRef`, `fieldRef`, ..., `compose`, `dockerfile`; values are never stored); and that container's `EnvVar` to the code reading the name inside the files its image is built from (`strategy: env_read`, with `dockerfile`): the image's files are the sources its Dockerfile's `COPY` / `ADD` put into it, less the context's `.dockerignore`
|
||||
|
||||
+126
-46
@@ -916,15 +916,17 @@ struct cbm_helm_defines {
|
||||
hd_file_t **files; /* each allocated once: defines point into them */
|
||||
int nfiles;
|
||||
int cap;
|
||||
CBMHashTable *names; /* name -> hd_def_t* */
|
||||
CBMHashTable *names; /* name -> hd_def_t* */
|
||||
CBMHashTable *by_rel; /* path -> hd_file_t* */
|
||||
};
|
||||
|
||||
cbm_helm_defines_t *cbm_helm_defines_new(void) {
|
||||
cbm_helm_defines_t *d = cbm_calloc(CBM_MEM_CLASS_OTHER, sizeof(*d));
|
||||
if (d) {
|
||||
d->names = cbm_ht_create(0);
|
||||
if (!d->names) {
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, d);
|
||||
d->by_rel = cbm_ht_create(0);
|
||||
if (!d->names || !d->by_rel) {
|
||||
cbm_helm_defines_free(d);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
@@ -1018,6 +1020,7 @@ void cbm_helm_defines_add(cbm_helm_defines_t *d, const char *rel, const char *sr
|
||||
ts_parser_delete(parser);
|
||||
}
|
||||
d->files[d->nfiles++] = f;
|
||||
cbm_ht_set(d->by_rel, f->rel, f);
|
||||
if (f->tree) {
|
||||
hd_scan(d, f, ts_tree_root_node(f->tree), hd_depth(rel));
|
||||
}
|
||||
@@ -1037,6 +1040,9 @@ void cbm_helm_defines_free(cbm_helm_defines_t *d) {
|
||||
cbm_ht_foreach(d->names, hd_free_def, NULL);
|
||||
cbm_ht_free(d->names);
|
||||
}
|
||||
if (d->by_rel) {
|
||||
cbm_ht_free(d->by_rel); /* its keys are the files' own */
|
||||
}
|
||||
for (int i = 0; i < d->nfiles; i++) {
|
||||
if (d->files[i]->tree) {
|
||||
ts_tree_delete(d->files[i]->tree);
|
||||
@@ -1051,12 +1057,7 @@ void cbm_helm_defines_free(cbm_helm_defines_t *d) {
|
||||
|
||||
/* Template file `rel` of the set; NULL: none. */
|
||||
static const hd_file_t *hd_file_find(const cbm_helm_defines_t *d, const char *rel) {
|
||||
for (int i = 0; d && i < d->nfiles; i++) {
|
||||
if (strcmp(d->files[i]->rel, rel) == 0) {
|
||||
return d->files[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
return d && d->by_rel ? cbm_ht_get(d->by_rel, rel) : NULL;
|
||||
}
|
||||
|
||||
static const hd_def_t *hd_find(const cbm_helm_defines_t *d, const char *name) {
|
||||
@@ -1394,6 +1395,11 @@ typedef struct {
|
||||
* and for a merge of values also by values path (everything below it) */
|
||||
CBMHashTable *poison_maps;
|
||||
CBMHashTable *poison_paths;
|
||||
/* the values path of each values map changed (a set or a merge): a merge
|
||||
* above one of them reaches what it put there */
|
||||
const char **changes;
|
||||
int nchanges;
|
||||
int chcap;
|
||||
/* a change of maps not run exactly: every capture ending later is UNKNOWN */
|
||||
bool tainted;
|
||||
CBMHashTable *mutates; /* define name -> it may change a map (he_define_may_mutate) */
|
||||
@@ -1655,6 +1661,37 @@ static bool hst_poison(hst_t *st, const hv_t *v, bool deep) {
|
||||
return hst_poison_at(st, v, deep, 0);
|
||||
}
|
||||
|
||||
/* Has a values map at path p or below it been changed? */
|
||||
static bool hst_changed_below(hst_t *st, const char *p) {
|
||||
size_t lp = strlen(p);
|
||||
for (int i = 0; i < st->nchanges; i++) {
|
||||
const char *c = st->changes[i];
|
||||
if (!hst_step(st) ||
|
||||
(strncmp(c, p, lp) == 0 && (c[lp] == '\0' || c[lp] == '.' || c[lp] == '['))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Values map at path p changed in place; false with no room to say so. */
|
||||
static bool hst_record_change(hst_t *st, const char *p) {
|
||||
if (st->nchanges == st->chcap) {
|
||||
int cap = st->chcap ? st->chcap * PAIR_LEN : CBM_SZ_16;
|
||||
const char **g = cbm_arena_alloc(st->a, (size_t)cap * sizeof(*g));
|
||||
if (!g) {
|
||||
return false;
|
||||
}
|
||||
if (st->nchanges) {
|
||||
memcpy(g, st->changes, (size_t)st->nchanges * sizeof(*g));
|
||||
}
|
||||
st->changes = g;
|
||||
st->chcap = cap;
|
||||
}
|
||||
st->changes[st->nchanges++] = p;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool hst_poison_at(hst_t *st, const hv_t *v, bool deep, int depth) {
|
||||
if (depth > HELM_MAX_NESTING || !hst_step(st)) {
|
||||
return false;
|
||||
@@ -1666,9 +1703,12 @@ static bool hst_poison_at(hst_t *st, const hv_t *v, bool deep, int depth) {
|
||||
return false;
|
||||
}
|
||||
/* a deep change reaches every map within: one not known, one a change
|
||||
* already made unknown, the values root (every path below it), or a map
|
||||
* not of the values (whose maps no path covers) cannot be poisoned whole */
|
||||
if (deep && (hv_poisoned(st, v) || (v->path && !v->path[0]) || (v->k == HV_JSON && !v->path))) {
|
||||
* already made unknown, the values root (every path below it), a map not
|
||||
* of the values (whose maps no path covers), or a values map below which
|
||||
* a change was made (it may hold another's map now) cannot be poisoned
|
||||
* whole */
|
||||
if (deep && (hv_poisoned(st, v) || (v->path && !v->path[0]) || (v->k == HV_JSON && !v->path) ||
|
||||
(v->path && hst_changed_below(st, v->path)))) {
|
||||
return false;
|
||||
}
|
||||
if (deep && v->k == HV_MAP) {
|
||||
@@ -1681,7 +1721,7 @@ static bool hst_poison_at(hst_t *st, const hv_t *v, bool deep, int depth) {
|
||||
}
|
||||
char key[CBM_SZ_32];
|
||||
snprintf(key, sizeof(key), "%p", map);
|
||||
if (!hst_set_add(st, &st->poison_maps, key)) {
|
||||
if (!hst_set_add(st, &st->poison_maps, key) || (v->path && !hst_record_change(st, v->path))) {
|
||||
return false;
|
||||
}
|
||||
return !(deep && v->path) || hst_set_add(st, &st->poison_paths, v->path);
|
||||
@@ -1935,12 +1975,15 @@ static void hst_capture_end(hst_t *st) {
|
||||
p->keys[p->n] = whole ? keys : NULL;
|
||||
p->n++;
|
||||
}
|
||||
/* the keys this capture recorded, one by one: a clear walks the set's
|
||||
* whole capacity, which one huge capture leaves behind for every later
|
||||
* one */
|
||||
for (int i = 0; st->ckset && i < st->nck; i++) {
|
||||
(void)cbm_ht_delete(st->ckset, st->ckeys[i]);
|
||||
}
|
||||
st->capture = -SKIP_ONE;
|
||||
st->cap = (hsink_t){0};
|
||||
st->nck = 0;
|
||||
if (st->ckset) {
|
||||
cbm_ht_clear(st->ckset);
|
||||
}
|
||||
}
|
||||
|
||||
/* Output of the top-level template: into the capture until its newline. */
|
||||
@@ -2415,10 +2458,16 @@ static hv_t he_strfn(hst_t *st, const char *fn, const hv_t *a, int n) {
|
||||
return HV_UNKNOWN;
|
||||
}
|
||||
|
||||
/* Sprig functions that change a map in place. */
|
||||
/* Sprig functions that change their argument in place: set / unset / merge
|
||||
* a map, sortAlpha a []string (splitList, keys, regexSplit give one; a list
|
||||
* is never poisoned, so it always counts as a change not run). Every other
|
||||
* list function copies its argument (push, append, prepend, reverse, uniq,
|
||||
* without, compact, rest, initial, concat, chunk) or reads it (first, last,
|
||||
* has, slice -- which shares it but changes nothing). */
|
||||
static bool he_mutator(const char *s, size_t n) {
|
||||
static const char *const MUTATORS[] = {
|
||||
"set", "unset", "merge", "mergeOverwrite", "mustMerge", "mustMergeOverwrite", NULL};
|
||||
"set", "unset", "merge", "mergeOverwrite", "mustMerge", "mustMergeOverwrite",
|
||||
"sortAlpha", NULL};
|
||||
return hy_in(s, n, MUTATORS);
|
||||
}
|
||||
|
||||
@@ -2478,8 +2527,8 @@ static bool he_is_fn(const char *s, size_t n, const char *name) {
|
||||
* looked into), or an include / template of a name not known or of one that
|
||||
* may (he_define_may_mutate). A variable's name ($set) is no call. */
|
||||
static bool he_may_mutate(hst_t *st, const char *src, TSNode n, int depth) {
|
||||
if (depth > HELM_MAX_NESTING) {
|
||||
return true;
|
||||
if (depth > HELM_MAX_NESTING || ts_node_has_error(n)) {
|
||||
return true; /* a form the grammar rejects (Go may not) may hide a change */
|
||||
}
|
||||
TSTreeCursor cur = ts_tree_cursor_new(n);
|
||||
bool found = false;
|
||||
@@ -2514,6 +2563,9 @@ static bool he_may_mutate(hst_t *st, const char *src, TSNode n, int depth) {
|
||||
return found || st->stop;
|
||||
}
|
||||
|
||||
/* Memo states of he_define_may_mutate / he_file_may_mutate. */
|
||||
enum { HM_NO = 1, HM_YES = 2, HM_BUSY = 3, HM_STALE = 4 };
|
||||
|
||||
/* The chart's template file an include names -- Helm names each one
|
||||
* `<.Template.BasePath>/<file>` -- or NULL: none of this chart's. */
|
||||
static const hd_file_t *he_template_file(hst_t *st, const char *name) {
|
||||
@@ -2534,7 +2586,6 @@ static const hd_file_t *he_template_file(hst_t *st, const char *name) {
|
||||
* while it is being scanned -- one including itself -- may. A no that leaned
|
||||
* on a define still being scanned is not kept. */
|
||||
static bool he_file_may_mutate(hst_t *st, const hd_file_t *f, int depth) {
|
||||
enum { HM_NO = 1, HM_YES = 2, HM_BUSY = 3 };
|
||||
if (!f->tree) {
|
||||
return true;
|
||||
}
|
||||
@@ -2564,14 +2615,6 @@ static bool he_file_may_mutate(hst_t *st, const hd_file_t *f, int depth) {
|
||||
* 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, HM_BUSY = 3, HM_STALE = 4 };
|
||||
const hd_def_t *d = hd_find(st->defs, name);
|
||||
if (!d) {
|
||||
/* a template file of the chart by its name (the checksum idiom) is
|
||||
* looked into; a name not the chart's (a library's) may */
|
||||
const hd_file_t *f = he_template_file(st, name);
|
||||
return f ? he_file_may_mutate(st, f, depth) : true;
|
||||
}
|
||||
if (!st->mutates) {
|
||||
st->mutates = cbm_ht_create(0);
|
||||
}
|
||||
@@ -2579,6 +2622,21 @@ static bool he_define_may_mutate(hst_t *st, const char *name, int depth) {
|
||||
if (memo == HM_YES || memo == HM_NO) {
|
||||
return memo == HM_YES;
|
||||
}
|
||||
const hd_def_t *d = hd_find(st->defs, name);
|
||||
if (!d) {
|
||||
/* a template file of the chart by its name (the checksum idiom) is
|
||||
* looked into; a name not the chart's (a library's) may. Kept by
|
||||
* name once final, so an include in a loop asks once */
|
||||
const hd_file_t *f = he_template_file(st, name);
|
||||
bool m = f ? he_file_may_mutate(st, f, depth) : true;
|
||||
bool final =
|
||||
m || (st->file_mutates && (uintptr_t)cbm_ht_get(st->file_mutates, f->rel) == HM_NO);
|
||||
const char *key = final && st->mutates ? cbm_arena_strdup(st->a, name) : NULL;
|
||||
if (key) {
|
||||
cbm_ht_set(st->mutates, key, (void *)(uintptr_t)(m ? HM_YES : HM_NO));
|
||||
}
|
||||
return m;
|
||||
}
|
||||
if (memo == HM_BUSY) {
|
||||
st->busy_hits++;
|
||||
return false;
|
||||
@@ -2988,7 +3046,15 @@ static hv_t he_call(hst_t *st, hframe_t *fr, TSNode call, const hv_t *piped) {
|
||||
uint32_t total = na + (piped ? SKIP_ONE : 0);
|
||||
for (uint32_t i = 0; i < total; i++) {
|
||||
v = i < na ? he_eval(st, fr, ts_node_named_child(args, i)) : *piped;
|
||||
if (v.k == HV_UNK) {
|
||||
if (v.k == HV_UNK || hv_poisoned(st, &v)) {
|
||||
/* its truth is not known (a poisoned map's either): whether
|
||||
* the operands after it run is not, nor a change they make */
|
||||
for (uint32_t j = i + SKIP_ONE; j < na; j++) {
|
||||
if (he_may_mutate(st, fr->src, ts_node_named_child(args, j), st->nesting)) {
|
||||
hst_taint(st, fn);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return HV_UNKNOWN;
|
||||
}
|
||||
bool t = hv_truth(&v);
|
||||
@@ -3212,20 +3278,25 @@ static int he_scan_skipped(hst_t *st, hframe_t *fr, TSNode n, bool in_range) {
|
||||
return leave;
|
||||
}
|
||||
|
||||
/* The children of n but its condition / collection (what was run), each:
|
||||
/* The children of n but what was run -- its collection, its conditions up
|
||||
* to `ran` (the one run; an else if / else with after it would be) -- each:
|
||||
* the variables it would assign UNKNOWN, and a change of maps it may make
|
||||
* (he_may_mutate) leaves every image after it UNKNOWN; 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) {
|
||||
static int he_spoil_parts(hst_t *st, hframe_t *fr, TSNode n, TSNode ran, const char *only,
|
||||
bool in_range) {
|
||||
int leave = HL_NONE;
|
||||
bool mutates = false;
|
||||
bool after = false; /* past `ran` */
|
||||
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 (!ts_node_is_named(c) || hn_is(c, "range_variable_definition") ||
|
||||
(f && (strcmp(f, "condition") == 0 || strcmp(f, "range") == 0)) ||
|
||||
(only && !(f && strcmp(f, only) == 0))) {
|
||||
bool cond = f && strcmp(f, "condition") == 0;
|
||||
bool was_run = cond && !after;
|
||||
after = after || (cond && ts_node_eq(c, ran));
|
||||
if (!ts_node_is_named(c) || hn_is(c, "range_variable_definition") || was_run ||
|
||||
(f && strcmp(f, "range") == 0) || (only && !(f && strcmp(f, only) == 0))) {
|
||||
continue;
|
||||
}
|
||||
leave |= he_scan_skipped(st, fr, c, in_range);
|
||||
@@ -3237,16 +3308,17 @@ static int he_spoil_parts(hst_t *st, hframe_t *fr, TSNode n, const char *only, b
|
||||
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) {
|
||||
/* The bodies of if / with / range n, not run because its condition `ran` or
|
||||
* its collection (a null `ran`) is UNKNOWN (that one was run): their output,
|
||||
* the variables they -- and the conditions after `ran` -- 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, TSNode ran) {
|
||||
hst_out_unknown(st, fr);
|
||||
bool range = hn_is(n, "range_action");
|
||||
int leave = he_spoil_parts(st, fr, n, NULL, range);
|
||||
int leave = he_spoil_parts(st, fr, n, ran, NULL, range);
|
||||
if (leave == HL_NONE) {
|
||||
return;
|
||||
}
|
||||
@@ -3257,7 +3329,7 @@ static void he_skipped(hst_t *st, hframe_t *fr, TSNode n) {
|
||||
loop = ts_node_parent(loop);
|
||||
}
|
||||
if (hn_is(loop, "range_action")) {
|
||||
(void)he_spoil_parts(st, fr, loop, "body", true);
|
||||
(void)he_spoil_parts(st, fr, loop, (TSNode){0}, "body", true);
|
||||
}
|
||||
if (leave & HL_BREAK) {
|
||||
st->brk = true;
|
||||
@@ -3293,7 +3365,7 @@ static void he_branches(hst_t *st, hframe_t *fr, TSNode n, bool with) {
|
||||
if (v.k == HV_UNK || hv_poisoned(st, &v)) {
|
||||
stop = true;
|
||||
active = false;
|
||||
he_skipped(st, fr, n); /* which branch renders is unknown */
|
||||
he_skipped(st, fr, n, c); /* which branch renders is unknown */
|
||||
continue;
|
||||
}
|
||||
active = hv_truth(&v);
|
||||
@@ -3331,7 +3403,7 @@ static void he_range(hst_t *st, hframe_t *fr, TSNode n) {
|
||||
}
|
||||
hv_t coll = he_eval(st, fr, expr);
|
||||
if (coll.k == HV_UNK || hv_poisoned(st, &coll)) {
|
||||
he_skipped(st, fr, n);
|
||||
he_skipped(st, fr, n, (TSNode){0});
|
||||
return;
|
||||
}
|
||||
hv_t *items = NULL;
|
||||
@@ -3472,6 +3544,9 @@ static hv_t he_render_define(hst_t *st, const char *name, hv_t arg, hsink_t *int
|
||||
hst_taint(st, "include"); /* nested too deep: not run */
|
||||
return HV_UNKNOWN;
|
||||
}
|
||||
if (ts_node_has_error(d->node)) {
|
||||
hst_taint(st, "parse"); /* a form the grammar rejects may hide a change */
|
||||
}
|
||||
int mark = st->nvars;
|
||||
hframe_t sub = {.src = d->file->src, .dot = arg, .var_base = st->nvars, .out = into};
|
||||
hst_var_push(st, "", 0, arg);
|
||||
@@ -3584,6 +3659,11 @@ bool cbm_helm_render(CBMArena *a, const char *rel, const char *src, size_t len,
|
||||
uint32_t *lines = cbm_arena_alloc(&work, (size_t)nl * sizeof(*lines));
|
||||
TSTree *tree = lines ? ts_parser_parse_string(st.parser, NULL, src, (uint32_t)len) : NULL;
|
||||
if (tree) {
|
||||
/* a template that does not parse may hide a change (Go may take a
|
||||
* form the grammar rejects): no image of it is known */
|
||||
if (ts_node_has_error(ts_tree_root_node(tree))) {
|
||||
hst_taint(&st, "parse");
|
||||
}
|
||||
int k = 0;
|
||||
lines[k++] = 0;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
|
||||
@@ -4947,7 +4947,14 @@ TEST(probe_helm_release_terraform_postrender_forms) {
|
||||
* 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). */
|
||||
* checksums its Secret).
|
||||
*
|
||||
* NOT a cross-file guarantee: app's cm.yaml sets .Values.image.tag, and Helm
|
||||
* renders a chart's files with one shared .Values (a subchart's is the
|
||||
* parent's .Values.<alias>). Each file renders alone here, so app's image is
|
||||
* known only because nothing models that set reaching deploy.yaml -- the
|
||||
* known limit (review round 4, R4-5), whose exact fix is to run the family's
|
||||
* files in Helm's order with one .Values (the run-included-files follow-up). */
|
||||
TEST(probe_helm_checksum_include_by_base_path) {
|
||||
static const GpfFile files[] = {
|
||||
{"charts/app/Chart.yaml", "apiVersion: v2\nname: app\nversion: 1.0.0\ndependencies:\n"
|
||||
|
||||
@@ -5050,6 +5050,176 @@ TEST(helm_template_file_including_itself) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
static const char HELM_IMAGE_VALUES[] =
|
||||
"image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\ncfg: {a: {}}\nempty: {}\n";
|
||||
#define HELM_IMAGE_LINE "image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"
|
||||
|
||||
/* A set below a values map puts .Values.image at cfg.a.d; a deep merge into
|
||||
* the map above it (.Values.cfg) reaches .Values.image there: the image after
|
||||
* it is UNKNOWN. */
|
||||
TEST(helm_deep_merge_over_a_change_below) {
|
||||
static const char tpl[] =
|
||||
"{{- $_ := set .Values.cfg.a \"d\" .Values.image }}\n"
|
||||
"{{- $_ := mergeOverwrite .Values.cfg (dict \"a\" (dict \"d\" (dict \"tag\" \"2.0\"))) }}\n"
|
||||
HELM_IMAGE_LINE;
|
||||
char *got = helm_capture(HELM_IMAGE_VALUES, tpl, NULL, 3);
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* and / or stop at an operand whose truth is not known (.Release.Namespace;
|
||||
* a map a set changed -- .Values.empty is empty no more): whether the
|
||||
* operands after it run is not known, nor a change they make -- the image
|
||||
* after it is UNKNOWN. A known false one runs none: the image stays known. */
|
||||
TEST(helm_and_or_with_an_unknown_operand) {
|
||||
static const char *const tpls[] = {
|
||||
"{{- $_ := and .Release.Namespace (set .Values.image \"tag\" \"2.0\") }}\n" HELM_IMAGE_LINE,
|
||||
"{{- $_ := or .Release.Namespace (set .Values.image \"tag\" \"2.0\") }}\n" HELM_IMAGE_LINE,
|
||||
"{{- $_ := set .Values.empty \"x\" \"y\" }}{{- $_ := and .Values.empty "
|
||||
"(set .Values.image \"tag\" \"2.0\") }}\n" HELM_IMAGE_LINE};
|
||||
for (size_t i = 0; i < sizeof(tpls) / sizeof(tpls[0]); i++) {
|
||||
char *got = helm_capture(HELM_IMAGE_VALUES, tpls[i], NULL, 2);
|
||||
bool unknown = strcmp(got, "<UNKNOWN>") == 0;
|
||||
free(got);
|
||||
ASSERT_TRUE(unknown);
|
||||
}
|
||||
static const char known[] =
|
||||
"{{- $_ := and false (set .Values.image \"tag\" \"2.0\") }}\n" HELM_IMAGE_LINE;
|
||||
char *got = helm_capture(HELM_IMAGE_VALUES, known, NULL, 2);
|
||||
ASSERT_STR_EQ(got, "ghcr.io/acme/app:1.0");
|
||||
free(got);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* An if whose first condition is UNKNOWN: Helm evaluates the else-if
|
||||
* conditions after it -- a set there, or an assignment ($tag = ...), may run:
|
||||
* the image after it is UNKNOWN. */
|
||||
TEST(helm_later_condition_of_a_skipped_if) {
|
||||
static const char set[] =
|
||||
"{{- if regexMatch \"^v\" .Values.image.tag }}{{- else if set .Values.image \"tag\" \"2.0\" }}"
|
||||
"{{- end }}\n" HELM_IMAGE_LINE;
|
||||
char *got = helm_capture(HELM_IMAGE_VALUES, set, NULL, 2);
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
static const char assign[] =
|
||||
"{{- $tag := \"1.0\" }}{{- if regexMatch \"^v\" .Values.image.tag }}"
|
||||
"{{- else if $tag = \"2.0\" }}{{- end }}\n"
|
||||
"image: \"ghcr.io/acme/app:{{ $tag }}\"\n";
|
||||
got = helm_capture(HELM_IMAGE_VALUES, assign, NULL, 2);
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* sortAlpha sorts a []string in place (splitList gives one): the list $tags
|
||||
* holds changes -- not run here, so the image after it is UNKNOWN. */
|
||||
TEST(helm_sort_alpha_changes_its_list) {
|
||||
static const char tpl[] = "{{- $tags := splitList \",\" .Values.tags }}"
|
||||
"{{- $_ := sortAlpha $tags }}\n"
|
||||
"image: \"ghcr.io/acme/app:{{ first $tags }}\"\n";
|
||||
char *got = helm_capture("tags: \"2.0,1.0\"\n", tpl, NULL, 2);
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A form the grammar rejects may hide a change (Go may take it): a template,
|
||||
* a define it runs, or a template file it includes that does not parse
|
||||
* leaves its images UNKNOWN. */
|
||||
TEST(helm_parse_error_spoils) {
|
||||
static const char bad[] = "{{- $x := }}\n" HELM_IMAGE_LINE;
|
||||
char *got = helm_capture(HELM_IMAGE_VALUES, bad, NULL, 2);
|
||||
ASSERT_STR_EQ(got, "<UNKNOWN>");
|
||||
free(got);
|
||||
static const char defs_src[] =
|
||||
"{{- define \"bad\" -}}{{- $x := }}{{- end -}}\n"
|
||||
"{{- define \"good\" -}}ok{{- end -}}\n";
|
||||
cbm_helm_defines_t *d = cbm_helm_defines_new();
|
||||
ASSERT_NOT_NULL(d);
|
||||
cbm_helm_defines_add(d, "charts/app/templates/_bad.tpl", defs_src, strlen(defs_src));
|
||||
static const char bad_file[] = "{{- $x := }}\nkind: ConfigMap\n";
|
||||
cbm_helm_defines_add(d, "charts/app/templates/bad.yaml", bad_file, strlen(bad_file));
|
||||
static const char good[] = "x: {{ include \"good\" . }}\n" HELM_IMAGE_LINE;
|
||||
static const char via_define[] = "x: {{ include \"bad\" . }}\n" HELM_IMAGE_LINE;
|
||||
static const char via_file[] =
|
||||
"x: {{ include (print $.Template.BasePath \"/bad.yaml\") . | sha256sum }}\n" HELM_IMAGE_LINE;
|
||||
char *ok = helm_capture_chart(HELM_IMAGE_VALUES, good, d, &HELM_APP, 2);
|
||||
char *by_define = helm_capture_chart(HELM_IMAGE_VALUES, via_define, d, &HELM_APP, 2);
|
||||
char *by_file = helm_capture_chart(HELM_IMAGE_VALUES, via_file, d, &HELM_APP, 2);
|
||||
cbm_helm_defines_free(d);
|
||||
ASSERT_STR_EQ(ok, "ghcr.io/acme/app:1.0");
|
||||
ASSERT_STR_EQ(by_define, "<UNKNOWN>");
|
||||
ASSERT_STR_EQ(by_file, "<UNKNOWN>");
|
||||
free(ok);
|
||||
free(by_define);
|
||||
free(by_file);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* One capture reading 20,000 keys, then 2,000 captures of two keys each:
|
||||
* every image and mark after the big one stays right (its keys leave the
|
||||
* set one by one; the set is never cleared by walking its capacity). */
|
||||
TEST(helm_captures_after_a_huge_capture) {
|
||||
enum { ITEMS = 20000, LATER = 2000 };
|
||||
size_t cap = (size_t)ITEMS * 8 + CBM_SZ_256;
|
||||
char *vals = malloc(cap);
|
||||
ASSERT_NOT_NULL(vals);
|
||||
size_t o = (size_t)snprintf(vals, cap, "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\n"
|
||||
"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");
|
||||
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: \"r/{{ range .Values.big }}{{ .x }}{{ end }}\"\n"
|
||||
"{{- range 2000 }}\n"
|
||||
"image: \"{{ $.Values.image.repository }}:{{ $.Values.image.tag }}\"\n"
|
||||
"{{- end }}\n";
|
||||
CBMArena a;
|
||||
cbm_arena_init_lazy(&a, CBM_SZ_4K);
|
||||
cbm_helm_probe_t probes[2] = {{.line = 1}, {.line = 3}};
|
||||
bool ok = cbm_helm_render(&a, "t.yaml", tpl, strlen(tpl), v, NULL, NULL, probes, 2);
|
||||
int n = probes[1].n;
|
||||
bool last = n == LATER && probes[1].image[n - 1] &&
|
||||
strcmp(probes[1].image[n - 1], "ghcr.io/acme/app:1.0") == 0 &&
|
||||
probes[1].keys[n - 1] &&
|
||||
strcmp(probes[1].keys[n - 1], "[\"image.repository\",\"image.tag\"]") == 0;
|
||||
cbm_arena_destroy(&a);
|
||||
if (doc) {
|
||||
yyjson_mut_doc_free(doc);
|
||||
}
|
||||
ASSERT_TRUE(ok);
|
||||
ASSERT_EQ(n, LATER);
|
||||
ASSERT_TRUE(last);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* 5,000 includes of one name in a loop -- a template file of the chart with
|
||||
* no change, or a name that is none -- get one answer each time (asked
|
||||
* once): the first spoils nothing, the second every image after it. */
|
||||
TEST(helm_includes_of_one_name_in_a_loop) {
|
||||
cbm_helm_defines_t *d = helm_chart_files();
|
||||
ASSERT_NOT_NULL(d);
|
||||
static const char clean[] =
|
||||
"{{- range 5000 }}{{ $_ := include (print $.Template.BasePath \"/secret.yaml\") $ }}"
|
||||
"{{ end }}\n" HELM_IMAGE_LINE;
|
||||
static const char missing[] =
|
||||
"{{- range 5000 }}{{ $_ := include (print $.Template.BasePath \"/missing.yaml\") $ }}"
|
||||
"{{ end }}\n" HELM_IMAGE_LINE;
|
||||
char *kept = helm_capture_chart(HELM_IMAGE_VALUES, clean, d, &HELM_APP, 2);
|
||||
char *spoiled = helm_capture_chart(HELM_IMAGE_VALUES, missing, d, &HELM_APP, 2);
|
||||
cbm_helm_defines_free(d);
|
||||
ASSERT_STR_EQ(kept, "ghcr.io/acme/app:1.0");
|
||||
ASSERT_STR_EQ(spoiled, "<UNKNOWN>");
|
||||
free(kept);
|
||||
free(spoiled);
|
||||
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. */
|
||||
@@ -24811,6 +24981,13 @@ SUITE(pipeline_semantic_manifest_repro) {
|
||||
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(helm_deep_merge_over_a_change_below);
|
||||
RUN_TEST(helm_and_or_with_an_unknown_operand);
|
||||
RUN_TEST(helm_later_condition_of_a_skipped_if);
|
||||
RUN_TEST(helm_sort_alpha_changes_its_list);
|
||||
RUN_TEST(helm_parse_error_spoils);
|
||||
RUN_TEST(helm_captures_after_a_huge_capture);
|
||||
RUN_TEST(helm_includes_of_one_name_in_a_loop);
|
||||
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