ci: hold every scalar that must be a string, and the required podAffinity
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 15s
Validate Helm Charts / Build paperless-ngx (pull_request) Successful in 9s
Validate Helm Charts / Resource quantities (pull_request) Successful in 15s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / Bump level (pull_request) Successful in 7s
Validate Helm Charts / YAML scalars (pull_request) Successful in 15s
Validate Helm Charts / Trivy config paperless-ngx (pull_request) Successful in 29s
Install-test / Install-test paperless-ngx (pull_request) Successful in 5m11s
Validate Helm Charts / Install-test (pull_request) Successful in 5m11s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 16s
Publish Paperless-NGX Helm Chart / Publish Paperless-NGX Helm Chart (push) Successful in 16s
SonarQube Analysis / SonarQube Analysis (push) Successful in 47s
Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 15s
Validate Helm Charts / Build paperless-ngx (pull_request) Successful in 9s
Validate Helm Charts / Resource quantities (pull_request) Successful in 15s
Validate Helm Charts / Selector match (pull_request) Successful in 14s
Validate Helm Charts / Bump level (pull_request) Successful in 7s
Validate Helm Charts / YAML scalars (pull_request) Successful in 15s
Validate Helm Charts / Trivy config paperless-ngx (pull_request) Successful in 29s
Install-test / Install-test paperless-ngx (pull_request) Successful in 5m11s
Validate Helm Charts / Install-test (pull_request) Successful in 5m11s
Publish Helm Chart with patched Application Version / publish-helm-chart (push) Successful in 16s
Publish Paperless-NGX Helm Chart / Publish Paperless-NGX Helm Chart (push) Successful in 16s
SonarQube Analysis / SonarQube Analysis (push) Successful in 47s
The test Gitea #11 asked for ("whoever fixes this ships a test case that would have caught it"), plus the guard for the fix #101 introduces. ci/run-yaml-scalar-render-test.sh renders every chart with the fixtures the install-test already uses and asserts three properties Kubernetes itself requires, so no per-chart expectations are needed: a CronJob schedule is a non-empty string, every env[].value of every container and initContainer is a string, and every label and annotation value is a string. The first catches the loud failure - the render aborting with a YAML error that names the template and not the value - and the other two catch the quiet one, where the render succeeds and the API server rejects the object at apply time. WHY A NEW JOB AND NOT THE INSTALL-TEST. The object that carried the defect is rendered by no fixture at all: paperless-ngx' consume CronJob sits behind smbConnection.enabled, false in the chart defaults and false in its install-test fixture, since the CI cluster has no SMB CSI driver. Nothing in this repository ever produced it. So the overlays under ci/yaml-scalar/ are the test, not the scaffolding: they switch such objects on AND feed them hostile values, and the script fails when an overlay stops reaching its object or when the run asserted nothing at all. The fail path was exercised in all three shapes against a throwaway copy of the chart as it stands on main, not reasoned about: the bare schedule aborted with "yaml: line 6: did not find expected alphabetic or numeric character"; with only the schedule quoted, the star-valued env entry aborted on the StatefulSet instead; and with only the bool-looking value left - a render that succeeds - the type assertion named the variable, the container and the API server's message. Green repo-wide otherwise: 30 charts, 408 scalars, 0 failed. ci/run-selector-match-test.sh additionally holds every requiredDuringSchedulingIgnoredDuringExecution podAffinity term against all pod templates of the same render (Gitea #101). That case is not the gap the script's header describes: a preference that cannot be met is dropped and an anti-affinity over zero pods is satisfied, but a REQUIRED term matching no pod can never be satisfied and the pod stays Pending for good. paperless-ngx' mover selects a label written in another template, which is exactly the copy this test holds - renaming it would leave both templates rendering perfectly while the mover silently stopped moving. Exercised by doing that rename in a throwaway copy: one FAIL naming the selector, the pod templates it missed and the labels actually present. Six charts still render their env block bare (matrix-synapse, minecraft-router, minecraft-server, rallly, satisfactory, teamspeak-server). They are green only because no fixture of theirs sets a hostile value; README now says so instead of repeating template-chart's claim that every chart quotes. Part of #11 Part of #101
This commit was merged in pull request #131.
This commit is contained in:
6 files changed
+622
-9
No files matched your search
@@ -52,10 +52,22 @@
|
||||
#
|
||||
# WHAT IS DELIBERATELY NOT CHECKED
|
||||
#
|
||||
# - podAffinity / podAntiAffinity. They carry labelSelectors too, but their
|
||||
# semantics are "relative to pods that exist", and an anti-affinity term
|
||||
# matching nothing is a legitimate configuration rather than a mistake.
|
||||
# Out of scope, and named so that the gap is not mistaken for coverage.
|
||||
# - preferred podAffinity and podAntiAffinity of either strength. They carry
|
||||
# labelSelectors too, but their semantics are "relative to pods that exist",
|
||||
# and a term matching nothing is a legitimate configuration rather than a
|
||||
# mistake: a preference that cannot be met is dropped and an anti-affinity
|
||||
# over zero pods is satisfied. Out of scope, and named so that the gap is
|
||||
# not mistaken for coverage.
|
||||
#
|
||||
# requiredDuringSchedulingIgnoredDuringExecution podAffinity is NOT in that
|
||||
# gap and is checked (Gitea #101). Its failure is not a preference quietly
|
||||
# dropped: a required term whose selector matches no pod in the release can
|
||||
# never be satisfied, so the pod stays Pending forever. For the workload
|
||||
# that introduced one here - paperless-ngx' consume CronJob, pinned onto the
|
||||
# node of its StatefulSet pod so that Longhorn will mount the shared RWX
|
||||
# volume - that would mean a mover that silently stops moving, with one
|
||||
# Pending pod per schedule and a green render behind it. The term names a
|
||||
# label written in ANOTHER template, which is the copy this test holds.
|
||||
# - selectors expressed ONLY through matchExpressions. Evaluating a set-based
|
||||
# requirement means reimplementing label-selector semantics, which is a
|
||||
# second parser by another name. Those are REPORTED as "not checked" and
|
||||
@@ -343,6 +355,55 @@ for doc in pdbs:
|
||||
", ".join("%s=%s" % kv for kv in sorted(match_labels.items())),
|
||||
len(all_templates), "; ".join(known) or "none"))
|
||||
|
||||
# 3. Every REQUIRED podAffinity term, against ALL pod templates of the render
|
||||
# (Gitea #101). Not against its own template: the whole point of such a term
|
||||
# is to select a DIFFERENT workload's pod, so the correct question is whether
|
||||
# anything in this release carries the labels it asks for. Preferred terms and
|
||||
# anti-affinity of either strength are left alone - see the header.
|
||||
for where, template in all_templates:
|
||||
affinity = template.get("spec", {}).get("affinity")
|
||||
if not isinstance(affinity, dict):
|
||||
continue
|
||||
pod_affinity = affinity.get("podAffinity")
|
||||
if not isinstance(pod_affinity, dict):
|
||||
continue
|
||||
terms = pod_affinity.get("requiredDuringSchedulingIgnoredDuringExecution")
|
||||
if not isinstance(terms, list):
|
||||
continue
|
||||
for i, term in enumerate(terms):
|
||||
if not isinstance(term, dict):
|
||||
lines.append("FAIL|%s: required podAffinity[%d] at %s is not a mapping"
|
||||
% (chart, i, where))
|
||||
continue
|
||||
tag = "required podAffinity[%d] on %s at %s" % (
|
||||
i, term.get("topologyKey", "<no topologyKey>"), where)
|
||||
match_labels, reason = classify(term.get("labelSelector"), tag)
|
||||
if match_labels is None:
|
||||
if term.get("labelSelector") is None:
|
||||
lines.append("FAIL|%s: %s" % (chart, reason))
|
||||
else:
|
||||
lines.append("SKIP|%s: %s" % (chart, reason))
|
||||
continue
|
||||
hits = [w for w, t in all_templates if matches(match_labels, labels_of(t))]
|
||||
if hits:
|
||||
lines.append("OK|%s: %s matches %d pod template(s): %s"
|
||||
% (chart, tag, len(hits), ", ".join(hits)))
|
||||
else:
|
||||
known = sorted({"%s=%s" % kv for _, t in all_templates for kv in labels_of(t).items()})
|
||||
lines.append("FAIL|%s: %s selects %s, which matches NONE of the %d "
|
||||
"pod template(s) in the same render. A REQUIRED "
|
||||
"podAffinity term that matches no pod can never be "
|
||||
"satisfied, so the pod it is attached to stays Pending "
|
||||
"forever - for a CronJob that means one Pending pod per "
|
||||
"schedule and a workload that silently stopped running, "
|
||||
"with nothing in the render to show for it. The usual "
|
||||
"cause is a label renamed in the template that carries "
|
||||
"the selected pod. Labels actually present on the pod "
|
||||
"templates: %s"
|
||||
% (chart, tag,
|
||||
", ".join("%s=%s" % kv for kv in sorted(match_labels.items())),
|
||||
len(all_templates), "; ".join(known) or "none"))
|
||||
|
||||
for line in lines:
|
||||
print(line)
|
||||
PY
|
||||
|
||||
Executable
+412
@@ -0,0 +1,412 @@
|
||||
#!/usr/bin/env bash
|
||||
# YAML-scalar render test: an operator value that must arrive in the manifest as
|
||||
# a STRING is held against the manifest it actually produced.
|
||||
#
|
||||
# Usage: ci/run-yaml-scalar-render-test.sh [chart ...] (no argument = all charts)
|
||||
#
|
||||
# WHAT IT GUARDS AGAINST (Gitea #11, and #112 turned out to be the same genus)
|
||||
#
|
||||
# A value is rendered into a YAML scalar without `| quote`. The operator quotes
|
||||
# it correctly in their values file, Helm passes the string's CONTENT through,
|
||||
# and the quotes are gone again in the manifest. Two outcomes, and the quiet one
|
||||
# is the worse one:
|
||||
#
|
||||
# 1. LOUD, but misdirected - the content is not a legal plain scalar and the
|
||||
# render aborts. Measured on paperless-ngx' consume CronJob, whose schedule
|
||||
# was unquoted: a schedule in the commonest cron notation of all (a star, a
|
||||
# slash, an interval) made `helm template` exit 1 with
|
||||
#
|
||||
# error converting YAML to JSON: yaml: line 6: did not find expected
|
||||
# alphabetic or numeric character
|
||||
#
|
||||
# YAML read the leading "*" as an alias indicator. The message names the
|
||||
# TEMPLATE and the operator's own value is nowhere in it, so the search
|
||||
# starts in the wrong place. "@daily" fails the same way.
|
||||
# 2. SILENT until the API server - the content IS a legal plain scalar, just
|
||||
# not a string. "true" becomes a bool, "0755" becomes an integer, and the
|
||||
# chart renders perfectly. Kubernetes then rejects the object with
|
||||
# "cannot unmarshal number into Go struct field EnvVar.value of type
|
||||
# string" - at apply time, which for this repository means in the
|
||||
# install-test or in a cluster, never in the render.
|
||||
#
|
||||
# Neither is caught by "does helm template exit 0" alone: the first one is, but
|
||||
# only if something actually renders the object, and the second one is not
|
||||
# caught at all.
|
||||
#
|
||||
# WHAT IS ASSERTED, and all three are properties Kubernetes itself requires -
|
||||
# there is no chart for which the opposite is a legitimate configuration, which
|
||||
# is why this needs no per-chart expectations:
|
||||
#
|
||||
# 1. CronJob .spec.schedule is a non-empty string.
|
||||
# 2. Every env[].value of every container and initContainer is a string.
|
||||
# 3. Every metadata.labels and metadata.annotations value is a string
|
||||
# (the API types both as map[string]string).
|
||||
#
|
||||
# WHY OVER ALL CHARTS, not the changed ones: the same reason as
|
||||
# ci/run-quantity-render-test.sh and ci/run-selector-match-test.sh. The env
|
||||
# block is copied into every chart rather than shared, so the unquoted form
|
||||
# travels by copy into whatever chart is started from an older one - and that
|
||||
# chart's own pull request is not where anyone would look for it. Running over
|
||||
# everything also means there is no list to maintain: adding a chart enrols it.
|
||||
#
|
||||
# THE OVERLAYS, AND WHY THIS TEST IS USELESS WITHOUT THEM
|
||||
#
|
||||
# The defect above survived in a shipped chart for months for one reason: the
|
||||
# object that carried it is rendered by NO fixture. paperless-ngx' CronJob sits
|
||||
# behind smbConnection.enabled, which is false in the chart defaults and in
|
||||
# every committed fixture, so neither lint, nor the render tests, nor the
|
||||
# install-test ever produced it. A test that renders only what the fixtures
|
||||
# already render would have been just as blind.
|
||||
#
|
||||
# So ci/yaml-scalar/<chart>.yaml is an overlay applied LAST whose job is to
|
||||
# switch such objects on AND to feed them the hostile values - a schedule
|
||||
# beginning with a star, an env value that is a bare "true". Two rules follow,
|
||||
# the same ones ci/run-selector-match-test.sh works under:
|
||||
#
|
||||
# 1. A chart that HAS an overlay and still renders none of the three things
|
||||
# above FAILS. The overlay exists for exactly that, so an overlay that
|
||||
# stopped reaching its object is a finding and not a shrug.
|
||||
# 2. If nothing at all was asserted across the whole run, the run fails. A
|
||||
# pass over zero assertions is not a pass.
|
||||
#
|
||||
# WHAT IS DELIBERATELY NOT CHECKED, so the gap is not mistaken for coverage:
|
||||
#
|
||||
# - Values rendered into a scalar that is hand-quoted in the template
|
||||
# ("{{ .Values.x }}"). Those are strings in the output, so this test is
|
||||
# blind to them by construction; what they can still do is break the YAML
|
||||
# when the value itself contains a quote or a backslash, and only `| quote`
|
||||
# fixes that. Reported here rather than silently implied.
|
||||
# - Everything that is not a label, an annotation, an env value or a schedule.
|
||||
# A bare `storage:` or `className:` is just as unquoted, but its value space
|
||||
# is DNS labels and quantities, where no legal value is YAML-ambiguous. The
|
||||
# three above are where operator free text actually lands.
|
||||
# - Whether a cron schedule is a VALID schedule. That is the cron
|
||||
# controller's judgement, and narrowing the schema to a regex was considered
|
||||
# and rejected: the defect was never a wrong schedule, it was a correct one
|
||||
# rendered bare.
|
||||
#
|
||||
# How the YAML is parsed: with helm, via ci/yaml2json, exactly as in
|
||||
# ci/run-selector-match-test.sh - the runners have neither pyyaml nor yq, and a
|
||||
# YAML reader written in awk here would fail by quietly reading nothing and
|
||||
# staying green. That script's header carries the full argument.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
CI_DIR="$ROOT/ci"
|
||||
YAML2JSON="$CI_DIR/yaml2json"
|
||||
OVERLAY_DIR="$CI_DIR/yaml-scalar"
|
||||
RELEASE="scalar"
|
||||
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
# --- Which charts -----------------------------------------------------------
|
||||
CHARTS=()
|
||||
if [ "$#" -gt 0 ]; then
|
||||
CHARTS=("$@")
|
||||
else
|
||||
for DIR in "$ROOT"/*/; do
|
||||
[ -f "$DIR/Chart.yaml" ] || continue
|
||||
CHARTS+=("$(basename "$DIR")")
|
||||
done
|
||||
fi
|
||||
if [ "${#CHARTS[@]}" -eq 0 ]; then
|
||||
echo "ERROR: no chart directories found under $ROOT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FAILED=""
|
||||
CHECKED=""
|
||||
NOT_EXERCISED=""
|
||||
TOTAL_ASSERTIONS=0
|
||||
SUMMARY_ROWS=""
|
||||
|
||||
for CHART in "${CHARTS[@]}"; do
|
||||
if [ ! -f "$ROOT/$CHART/Chart.yaml" ]; then
|
||||
echo "ERROR: '$CHART' is not a chart directory" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# A chart with a declared dependency cannot be rendered at all while charts/
|
||||
# is empty, and this job runs over EVERY chart on EVERY pull request.
|
||||
"$ROOT/ci/helm-dependencies.sh" "$ROOT/$CHART"
|
||||
|
||||
VALUES_ARGS=()
|
||||
VALUES_LABEL="chart defaults"
|
||||
if [ -f "$CI_DIR/$CHART/test-values.yaml" ]; then
|
||||
VALUES_ARGS=(-f "$CI_DIR/$CHART/test-values.yaml")
|
||||
VALUES_LABEL="ci/$CHART/test-values.yaml"
|
||||
elif [ -f "$CI_DIR/trivy/values/$CHART.yaml" ]; then
|
||||
VALUES_ARGS=(-f "$CI_DIR/trivy/values/$CHART.yaml")
|
||||
VALUES_LABEL="ci/trivy/values/$CHART.yaml"
|
||||
fi
|
||||
HAS_OVERLAY=""
|
||||
if [ -f "$OVERLAY_DIR/$CHART.yaml" ]; then
|
||||
# Last, so it wins over the install-test values.
|
||||
VALUES_ARGS+=(-f "$OVERLAY_DIR/$CHART.yaml")
|
||||
VALUES_LABEL="$VALUES_LABEL + ci/yaml-scalar/$CHART.yaml"
|
||||
HAS_OVERLAY=1
|
||||
fi
|
||||
|
||||
echo "::group::$CHART ($VALUES_LABEL)"
|
||||
|
||||
RENDER="$WORK/$CHART.manifest.yaml"
|
||||
if ! helm template "$RELEASE" "$ROOT/$CHART" \
|
||||
${VALUES_ARGS[@]+"${VALUES_ARGS[@]}"} >"$RENDER" 2>"$WORK/err"; then
|
||||
sed 's/^/ /' "$WORK/err"
|
||||
echo "::endgroup::"
|
||||
echo "::error title=yaml-scalar $CHART::$CHART does not render with these values. If the error is a YAML parse error naming a template line, that IS the finding this test exists for: a value reached a scalar unquoted. Reproduce: helm template $RELEASE $CHART $(printf '%s ' "${VALUES_ARGS[@]+${VALUES_ARGS[@]}}")"
|
||||
FAILED="$FAILED $CHART"
|
||||
continue
|
||||
fi
|
||||
|
||||
# The document split, the only textual step. A wrong split cannot produce a
|
||||
# false pass: the result stops being parsable and helm exits 1.
|
||||
DOCS="$WORK/$CHART.docs.yaml"
|
||||
awk '
|
||||
BEGIN { print "docs:"; started = 0 }
|
||||
/^---[ \t]*$/ { print " -"; started = 1; next }
|
||||
{
|
||||
if (!started) { print " -"; started = 1 }
|
||||
if (length($0) == 0) print ""; else print " " $0
|
||||
}
|
||||
' "$RENDER" >"$DOCS"
|
||||
|
||||
JSON="$WORK/$CHART.json"
|
||||
if ! helm template y2j "$YAML2JSON" -f "$DOCS" 2>"$WORK/err" \
|
||||
| awk '/^\{/ { print; exit }' >"$JSON"; then
|
||||
sed 's/^/ /' "$WORK/err"
|
||||
echo "::endgroup::"
|
||||
echo "::error title=yaml-scalar $CHART::the rendered manifests of $CHART could not be re-read as YAML by ci/yaml2json. That is a parse failure, not a scalar finding."
|
||||
FAILED="$FAILED $CHART"
|
||||
continue
|
||||
fi
|
||||
if [ ! -s "$JSON" ]; then
|
||||
echo "::endgroup::"
|
||||
echo "::error title=yaml-scalar $CHART::ci/yaml2json produced no JSON for $CHART - the manifests were not re-read, so nothing was checked."
|
||||
FAILED="$FAILED $CHART"
|
||||
continue
|
||||
fi
|
||||
|
||||
# --- The comparison --------------------------------------------------------
|
||||
# Standard library only, and it never decides the exit code itself: it writes
|
||||
# "VERDICT|detail" lines and the loop below decides, so a verdict can never
|
||||
# be lost to a swallowed status.
|
||||
set +e
|
||||
python3 - "$CHART" "$JSON" >"$WORK/$CHART.out" 2>"$WORK/$CHART.pyerr" <<'PY'
|
||||
import json, sys
|
||||
|
||||
chart, path = sys.argv[1], sys.argv[2]
|
||||
docs = json.load(open(path)).get("docs") or []
|
||||
lines = []
|
||||
|
||||
|
||||
def name_of(doc):
|
||||
meta = doc.get("metadata")
|
||||
if isinstance(meta, dict) and meta.get("name"):
|
||||
return str(meta["name"])
|
||||
return "<unnamed %s>" % doc.get("kind", "object")
|
||||
|
||||
|
||||
def typename(value):
|
||||
return {bool: "bool", int: "int", float: "float", type(None): "null",
|
||||
dict: "mapping", list: "list"}.get(type(value), type(value).__name__)
|
||||
|
||||
|
||||
def pod_templates(node, path="$"):
|
||||
"""Any value of a key named "template" carrying a spec with containers.
|
||||
Found by walking, so a CronJob's spec.jobTemplate.spec.template needs no
|
||||
special case and neither does whatever workload kind a chart adds next."""
|
||||
out = []
|
||||
if isinstance(node, dict):
|
||||
for key, value in node.items():
|
||||
here = "%s.%s" % (path, key)
|
||||
if (key == "template" and isinstance(value, dict)
|
||||
and isinstance(value.get("spec"), dict)
|
||||
and "containers" in value["spec"]):
|
||||
out.append((here, value))
|
||||
out.extend(pod_templates(value, here))
|
||||
elif isinstance(node, list):
|
||||
for i, value in enumerate(node):
|
||||
out.extend(pod_templates(value, "%s[%d]" % (path, i)))
|
||||
return out
|
||||
|
||||
|
||||
def metadatas(node, path="$"):
|
||||
"""Every metadata mapping anywhere in the render - top level and inside pod
|
||||
templates both, because labels and annotations exist at both levels."""
|
||||
out = []
|
||||
if isinstance(node, dict):
|
||||
for key, value in node.items():
|
||||
here = "%s.%s" % (path, key)
|
||||
if key == "metadata" and isinstance(value, dict):
|
||||
out.append((here, value))
|
||||
out.extend(metadatas(value, here))
|
||||
elif isinstance(node, list):
|
||||
for i, value in enumerate(node):
|
||||
out.extend(metadatas(value, "%s[%d]" % (path, i)))
|
||||
return out
|
||||
|
||||
|
||||
# --- 1. CronJob schedules ---------------------------------------------------
|
||||
for doc in docs:
|
||||
if not isinstance(doc, dict) or doc.get("kind") != "CronJob":
|
||||
continue
|
||||
where = "CronJob %s .spec.schedule" % name_of(doc)
|
||||
spec = doc.get("spec") if isinstance(doc.get("spec"), dict) else {}
|
||||
schedule = spec.get("schedule", "<absent>")
|
||||
if isinstance(schedule, str) and schedule.strip():
|
||||
lines.append("OK|%s: %s is the string %r" % (chart, where, schedule))
|
||||
elif schedule == "<absent>":
|
||||
lines.append("FAIL|%s: %s is missing entirely - a CronJob without a "
|
||||
"schedule is rejected by the API server" % (chart, where))
|
||||
else:
|
||||
lines.append("FAIL|%s: %s rendered as %s (%r), not a string. A schedule "
|
||||
"is operator free text and has to leave the template "
|
||||
"through | quote; without it YAML re-reads the content and "
|
||||
"a schedule beginning with a digit survives by luck while "
|
||||
"one beginning with * or @ does not"
|
||||
% (chart, where, typename(schedule), schedule))
|
||||
|
||||
# --- 2. env values ----------------------------------------------------------
|
||||
for where, template in pod_templates({"docs": docs}):
|
||||
spec = template.get("spec") if isinstance(template.get("spec"), dict) else {}
|
||||
for kind in ("initContainers", "containers"):
|
||||
for container in spec.get(kind) or []:
|
||||
if not isinstance(container, dict):
|
||||
continue
|
||||
cname = container.get("name", "<unnamed>")
|
||||
for entry in container.get("env") or []:
|
||||
if not isinstance(entry, dict) or "value" not in entry:
|
||||
continue # valueFrom entries carry no literal to check
|
||||
value = entry["value"]
|
||||
tag = "env %s of container %s at %s" % (
|
||||
entry.get("name", "<unnamed>"), cname, where)
|
||||
if isinstance(value, str):
|
||||
lines.append("OK|%s: %s is a string" % (chart, tag))
|
||||
else:
|
||||
lines.append("FAIL|%s: %s rendered as %s (%r), not a string. "
|
||||
"Kubernetes types env[].value as a string and "
|
||||
"the API server rejects the whole manifest with "
|
||||
"\"cannot unmarshal %s into Go struct field "
|
||||
"EnvVar.value of type string\" - at apply time, "
|
||||
"not in the render. The fix is | quote at the "
|
||||
"render site; template-chart carries the form"
|
||||
% (chart, tag, typename(value), value,
|
||||
"number" if not isinstance(value, bool) else "bool"))
|
||||
|
||||
# --- 3. labels and annotations ----------------------------------------------
|
||||
for where, meta in metadatas({"docs": docs}):
|
||||
for field in ("labels", "annotations"):
|
||||
block = meta.get(field)
|
||||
if not isinstance(block, dict):
|
||||
continue
|
||||
for key, value in block.items():
|
||||
tag = "%s[%r] at %s" % (field, key, where)
|
||||
if isinstance(value, str):
|
||||
lines.append("OK|%s: %s is a string" % (chart, tag))
|
||||
else:
|
||||
lines.append("FAIL|%s: %s rendered as %s (%r), not a string. "
|
||||
"Kubernetes types %s as map[string]string, so the "
|
||||
"API server rejects the object - the render stays "
|
||||
"green. The fix is | quote at the render site"
|
||||
% (chart, tag, typename(value), value, field))
|
||||
|
||||
for line in lines:
|
||||
print(line)
|
||||
PY
|
||||
RC=$?
|
||||
set -e
|
||||
if [ "$RC" -gt 1 ]; then
|
||||
sed 's/^/ /' "$WORK/$CHART.pyerr"
|
||||
echo "::endgroup::"
|
||||
echo "::error title=yaml-scalar $CHART::the scalar comparison itself failed for $CHART (python exit $RC). Treated as a failure, never as a pass."
|
||||
FAILED="$FAILED $CHART"
|
||||
continue
|
||||
fi
|
||||
|
||||
CHART_FAIL=""
|
||||
CHART_OK=0
|
||||
while IFS='|' read -r VERDICT DETAIL; do
|
||||
case "$VERDICT" in
|
||||
OK)
|
||||
CHART_OK=$((CHART_OK + 1))
|
||||
echo " ok $DETAIL"
|
||||
;;
|
||||
FAIL)
|
||||
CHART_FAIL=1
|
||||
echo " FAIL $DETAIL"
|
||||
echo "::error title=yaml-scalar $CHART::$DETAIL"
|
||||
;;
|
||||
*)
|
||||
[ -n "$VERDICT" ] && echo " $VERDICT $DETAIL"
|
||||
;;
|
||||
esac
|
||||
done <"$WORK/$CHART.out"
|
||||
echo "::endgroup::"
|
||||
|
||||
TOTAL_ASSERTIONS=$((TOTAL_ASSERTIONS + CHART_OK))
|
||||
|
||||
if [ -n "$CHART_FAIL" ]; then
|
||||
FAILED="$FAILED $CHART"
|
||||
SUMMARY_ROWS="${SUMMARY_ROWS}| \`$CHART\` | **failed** | $CHART_OK |"$'\n'
|
||||
echo "$CHART: FAILED"
|
||||
elif [ "$CHART_OK" -gt 0 ]; then
|
||||
CHECKED="$CHECKED $CHART"
|
||||
SUMMARY_ROWS="${SUMMARY_ROWS}| \`$CHART\` | ok | $CHART_OK |"$'\n'
|
||||
echo "$CHART: ok ($CHART_OK scalar(s) checked)"
|
||||
elif [ -n "$HAS_OVERLAY" ]; then
|
||||
FAILED="$FAILED $CHART"
|
||||
SUMMARY_ROWS="${SUMMARY_ROWS}| \`$CHART\` | **failed** | 0 |"$'\n'
|
||||
echo "::error title=yaml-scalar $CHART::$CHART has an overlay at ci/yaml-scalar/$CHART.yaml but its render carried no schedule, no env value and no label or annotation at all, so nothing was checked. That overlay exists only to make such a scalar render; a green tick here would be a tick for a check that ran over nothing."
|
||||
echo "$CHART: FAILED (overlay present, nothing asserted)"
|
||||
else
|
||||
NOT_EXERCISED="$NOT_EXERCISED $CHART"
|
||||
echo "$CHART: nothing of the three kinds rendered - nothing to check."
|
||||
fi
|
||||
done
|
||||
|
||||
# --- Report ------------------------------------------------------------------
|
||||
count() { set -- $1; echo "$#"; }
|
||||
listing() { set -- $1; [ "$#" -eq 0 ] && { echo "–"; return; }; printf '`%s`' "$1"; shift; for C in "$@"; do printf ', `%s`' "$C"; done; echo; }
|
||||
SUMMARY="${GITHUB_STEP_SUMMARY:-/dev/null}"
|
||||
{
|
||||
echo "### YAML-scalar render test (Gitea #11)"
|
||||
echo
|
||||
echo "Every \`CronJob.spec.schedule\`, every \`env[].value\` and every label and"
|
||||
echo "annotation value in a chart's render, held against the type Kubernetes requires"
|
||||
echo "for it. A value rendered without \`| quote\` either aborts the render with an"
|
||||
echo "error pointing at the template instead of at the value, or arrives as a bool or"
|
||||
echo "an integer and is rejected by the API server at apply time - never in the render."
|
||||
echo
|
||||
echo "| Result | Count | Charts |"
|
||||
echo "|---|---|---|"
|
||||
echo "| Scalars checked | $(count "$CHECKED") | $(listing "$CHECKED") |"
|
||||
echo "| Nothing of the kind rendered | $(count "$NOT_EXERCISED") | $(listing "$NOT_EXERCISED") |"
|
||||
echo "| Failed | $(count "$FAILED") | $(listing "$FAILED") |"
|
||||
echo
|
||||
echo "$TOTAL_ASSERTIONS scalar(s) verified in total."
|
||||
if [ -n "$SUMMARY_ROWS" ]; then
|
||||
echo
|
||||
echo "| Chart | Result | Checked |"
|
||||
echo "| --- | --- | --- |"
|
||||
printf '%s' "$SUMMARY_ROWS"
|
||||
fi
|
||||
} >>"$SUMMARY"
|
||||
|
||||
echo "===================== yaml scalar render test ======================"
|
||||
echo "checked: $(count "$CHECKED")${CHECKED:+ -$CHECKED}"
|
||||
echo "not exercised: $(count "$NOT_EXERCISED")${NOT_EXERCISED:+ -$NOT_EXERCISED}"
|
||||
echo "failed: $(count "$FAILED")${FAILED:+ -$FAILED}"
|
||||
echo "scalars verified: $TOTAL_ASSERTIONS"
|
||||
echo "==================================================================="
|
||||
|
||||
if [ -n "$FAILED" ]; then
|
||||
echo "ERROR: a value that has to arrive as a string did not, in:$FAILED" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$TOTAL_ASSERTIONS" -eq 0 ]; then
|
||||
echo "ERROR: not a single scalar was checked in this run. Either no chart renders a schedule, an env value, a label or an annotation any more, or the overlays under ci/yaml-scalar/ no longer reach their objects. A pass over zero assertions is not a pass." >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,25 @@
|
||||
# Selector-match overlay for paperless-ngx (Gitea #101).
|
||||
#
|
||||
# Its one job: make the consume CronJob render, so that the required podAffinity
|
||||
# it carries is actually held against the StatefulSet's pod labels. The template
|
||||
# sits behind smbConnection.enabled, false in the chart defaults and in
|
||||
# ci/paperless-ngx/test-values.yaml, so without this overlay the affinity term
|
||||
# exists in the repository and in no render - and the test would pass over it.
|
||||
#
|
||||
# What it guards: the term selects the StatefulSet pod by a label written in
|
||||
# ANOTHER template (paperless-server.sfs.yaml). Renaming that label leaves both
|
||||
# files rendering perfectly while the mover can never be scheduled again, one
|
||||
# Pending pod per schedule, nothing in the render to show for it.
|
||||
#
|
||||
# Kept separate from ci/yaml-scalar/paperless-ngx.yaml although both switch SMB
|
||||
# on: that one feeds the CronJob a hostile SCHEDULE and hostile env values for a
|
||||
# different test, and folding the two would make each test's failure depend on
|
||||
# the other's fixture.
|
||||
smbConnection:
|
||||
enabled: true
|
||||
server: "//smb.ci.local/scans"
|
||||
|
||||
secrets:
|
||||
# Satisfies the required() behind the SMB PersistentVolume's
|
||||
# nodeStageSecretRef. Nothing is mounted in a render.
|
||||
smbConnectionSecretRef: op://ci/paperless-smb
|
||||
@@ -0,0 +1,53 @@
|
||||
# YAML-scalar overlay for paperless-ngx (Gitea #11).
|
||||
#
|
||||
# THIS FILE IS THE TEST CASE the issue asked for, and its first job is to make
|
||||
# an object exist at all. templates/paperless-consume.cronjob.yaml sits behind
|
||||
# smbConnection.enabled, which is false in the chart defaults and false in
|
||||
# ci/paperless-ngx/test-values.yaml - deliberately, because the install-test has
|
||||
# no SMB CSI driver. So before this overlay NOTHING in the repository rendered
|
||||
# that CronJob: not lint, not the render tests, not the install-test. That is
|
||||
# the whole reason an unquoted schedule survived in a shipped chart, and it is
|
||||
# why the fix comes with a render test rather than with a fixture change.
|
||||
#
|
||||
# The values below are hostile on purpose. Against the chart before 13.0.0 this
|
||||
# overlay does not produce a finding - it produces an aborted render, which this
|
||||
# test reports as the finding it is:
|
||||
#
|
||||
# Error: YAML parse error on paperless-ngx/templates/paperless-consume.cronjob.yaml:
|
||||
# error converting YAML to JSON: yaml: line 6: did not find expected
|
||||
# alphabetic or numeric character
|
||||
smbConnection:
|
||||
enabled: true
|
||||
server: "//smb.ci.local/scans"
|
||||
# A schedule beginning with "*" - the commonest cron notation there is, and
|
||||
# the one that YAML reads as an alias indicator. Written here as a quoted
|
||||
# string, which is exactly how an operator would write it and exactly what
|
||||
# does NOT help: Helm passes the content through, and without | quote in the
|
||||
# template the star stands bare in the manifest again.
|
||||
cronJob: "*/15 * * * *"
|
||||
|
||||
secrets:
|
||||
# The CronJob's SMB volume has a required() secret ref behind it; the
|
||||
# PersistentVolume names it as nodeStageSecretRef. Nothing is mounted in a
|
||||
# render, so no fixture Secret is needed - only a value that satisfies the
|
||||
# required().
|
||||
smbConnectionSecretRef: op://ci/paperless-smb
|
||||
|
||||
paperless:
|
||||
# The silent half of the genus, on the chart's additive env block: both of
|
||||
# these are STRINGS in this file and both are schema-valid, and before 13.0.0
|
||||
# the first one aborted the render while the second arrived in the manifest as
|
||||
# a YAML bool - which the API server rejects at apply time with "cannot
|
||||
# unmarshal bool into Go struct field EnvVar.value of type string", so the
|
||||
# render test is the only place it can be caught cheaply.
|
||||
env:
|
||||
- name: PAPERLESS_STAR_VALUE
|
||||
value: "*never-interpreted*"
|
||||
- name: PAPERLESS_BOOLEAN_LOOKING_VALUE
|
||||
value: "true"
|
||||
# The variable the whole env block was asked for (Gitea #112): the log off
|
||||
# the NFS-backed RWX data volume and onto the /tmp emptyDir this pod already
|
||||
# mounts. Harmless in a render, and it keeps the worked example honest by
|
||||
# being the one an operator will actually copy.
|
||||
- name: PAPERLESS_LOGGING_DIR
|
||||
value: "/tmp/paperless-log"
|
||||
Reference in new issue
Block a user