Files
cozystack/hack/select-install.sh
Aleksei Sviridkin a6ec30d56f fix(ci): discover Chainsaw suites written as chainsaw-test.yml
Chainsaw loads chainsaw-test.yml when a directory has no
chainsaw-test.yaml, but select-e2e and select-install enumerated the
.yaml name only. A suite spelled that way ran whenever the whole
directory was handed to chainsaw, yet no escalation listed it and no
edit to it could select it, so it silently dropped out of every
narrowed run.

select-install also sent find's errors to /dev/null inside a pipeline
that reported sort's status, so a failed listing validated no suite at
all and passed. It is now captured and fails validation.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-01 01:06:24 +03:00

230 lines
9.6 KiB
Bash
Executable File

#!/bin/sh
# Given the E2E app suite(s) about to run, emit the minimal set of packages that
# must be INSTALLED for those suites — the forward dependency closure over the
# PackageSource graph. This is the install-side companion to select-e2e.sh:
# select-e2e.sh picks WHICH Chainsaw suites run (reverse dependency walk);
# select-install.sh picks WHAT must be up for them (forward dependency walk).
#
# Usage:
# hack/select-install.sh <suites> [<sources-dir>]
# hack/select-install.sh --validate [<sources-dir> [<suites-dir>]]
#
# Defaults: sources-dir = packages/core/platform/sources
# suites-dir = hack/e2e-chainsaw
#
# <suites>: whitespace-separated suite names, i.e. the output of select-e2e.sh
# (e.g. "postgres harbor"). "-" reads the list from stdin.
#
# Output (closure mode): space-separated PackageSource names (cozystack.*) that
# the run must enable, seeds + the engine + all transitive dependsOn. Empty
# when <suites> is empty. A suite with no known mapping is a HARD ERROR (fail
# closed) — a silently-skipped suite would install nothing and let the test
# pass vacuously or fail later on a missing CRD.
#
# --validate: check the whole graph AND the suite mapping — every dependsOn
# target resolves to a real PackageSource, there are no dependency cycles, and
# every Chainsaw suite under <suites-dir> resolves via suite_to_source() to a
# real PackageSource. Exit non-zero on any failure. The graph lives
# next to the packages and cannot drift from the suite directories; the
# hand-maintained suite mapping is the only part that can, so it is guarded here.
#
# Unlike select-e2e.sh's reverse walk, the forward walk KEEPS the
# cozystack.cozystack-engine edge: every *-application declares
# `dependsOn: cozystack.cozystack-engine` because the app's *-rd HelmRelease
# needs the engine to register the ApplicationDefinition CRD before it can
# reconcile. For test SELECTION that edge is noise (it would fan every app out
# to every other); for INSTALL it is a genuine prerequisite that must be up.
# Suites backed by a system package (e.g. securitygroup) carry no such edge, so
# the engine is seeded explicitly for any non-empty closure (see below).
set -eu
SOURCES_DIR="packages/core/platform/sources"
SUITES_DIR="hack/e2e-chainsaw"
MODE="closure"
SUITES=""
if [ "${1:-}" = "--validate" ]; then
MODE="validate"
SOURCES_DIR="${2:-$SOURCES_DIR}"
SUITES_DIR="${3:-$SUITES_DIR}"
else
suites_arg="${1?usage: select-install.sh <suites> [sources-dir] | --validate [sources-dir [suites-dir]]}"
SOURCES_DIR="${2:-$SOURCES_DIR}"
if [ "$suites_arg" = "-" ]; then
SUITES="$(cat)"
else
SUITES="$suites_arg"
fi
fi
# suite name -> owning PackageSource. Prints:
# - a "cozystack.*" name the suite's primary install target
# - nothing no mapping is known (caller fails closed)
# Most suites follow the <suite>-application convention; a bare-<suite> fallback
# and the explicit cases cover suites whose source is named differently. Kept in
# lockstep with select-e2e.sh's src_to_suites() and the hack/e2e-chainsaw/ dirs;
# --validate asserts every suite dir still resolves here.
#
# Note: a suite may exercise fixtures or selectors that name other app kinds
# (e.g. securitygroup's SecurityGroup references Postgres/Kubernetes by kind and
# name as selectors — it does not stand those apps up). Only the primary package
# is mapped here; the forward closure covers its own deps, the seeded engine
# covers the aggregated *.cozystack.io APIs, and the full-suite fallback covers
# the rest. Modelling multi-package suites is deferred to the test-minimal work.
suite_to_source() {
case "$1" in
kubernetes-latest|kubernetes-previous)
echo cozystack.kubernetes-application ; return ;;
vminstance) echo cozystack.vm-instance-application ; return ;;
securitygroup) echo cozystack.securitygroup-controller ; return ;;
kafka-metadata) echo cozystack.kafka-application ; return ;;
esac
for cand in "cozystack.$1-application" "cozystack.$1"; do
if echo "$NODES" | grep -Fxq "$cand"; then echo "$cand"; return; fi
done
}
# yq: forward edges — "owner<TAB>dep", one per variant dependsOn entry.
# NOTE: this unions dependsOn across ALL spec.variants[] rather than the single
# variant actually installed (e.g. cozystack.networking has 6 variants; only 5
# declare gateway-api-crds). That OVER-selects, never under-selects, so the
# emitted set is a safe superset of the true minimal set for the chosen variant.
build_forward_deps() {
yq -rN '.metadata.name as $n | .spec.variants[]?.dependsOn[]? | select(. != null and . != "") | $n + "\t" + .' "$SOURCES_DIR"/*.yaml
}
NODES="$(yq -rN '.metadata.name | select(. != null and . != "")' "$SOURCES_DIR"/*.yaml | sort -u)"
FORWARD="$(build_forward_deps | sort -u)"
# forward deps of a single node
deps_of() {
echo "$FORWARD" | awk -v s="$1" -F'\t' '$1==s {print $2}'
}
# all Chainsaw suite names under a suites-dir (dirs holding chainsaw-test.yaml
# or chainsaw-test.yml), discovered exactly like select-e2e.sh. find is
# captured on its own because a pipeline reports sort's status, and a failed
# listing must not read as an empty suites dir.
discover_suites() {
found=$(find "$1" -mindepth 2 -maxdepth 2 \( -name chainsaw-test.yaml -o -name chainsaw-test.yml \)) || return 1
printf '%s\n' "$found" | sed -e 's,/chainsaw-test\.yaml$,,' -e 's,/chainsaw-test\.yml$,,' -e 's,.*/,,' -e '/^$/d' | sort -u
}
if [ "$MODE" = "validate" ]; then
rc=0
# 1. Reachability: every dependsOn target must be a real PackageSource.
echo "$FORWARD" | awk -F'\t' '{print $2}' | sort -u | while IFS= read -r dep; do
[ -z "$dep" ] && continue
if ! echo "$NODES" | grep -Fxq "$dep"; then
owners="$(echo "$FORWARD" | awk -v x="$dep" -F'\t' '$2==x {print $1}' | paste -sd ',' -)"
echo "select-install: dangling dependency '$dep' (referenced by: $owners) has no PackageSource" >&2
echo dangling
fi
done | grep -q dangling && rc=1
# 2. Cycles: from every node with outgoing edges, walk forward; if the walk
# returns to its start, the graph has a cycle through it.
owners_list="$(echo "$FORWARD" | awk -F'\t' '{print $1}' | sort -u)"
for start in $owners_list; do
visited=""
frontier="$(deps_of "$start")"
while [ -n "$frontier" ]; do
next=""
for f in $frontier; do
if [ "$f" = "$start" ]; then
echo "select-install: dependency cycle detected involving '$start'" >&2
rc=1
next=""
break
fi
case " $visited " in *" $f "*) continue ;; esac
visited="$visited $f"
next="$next $(deps_of "$f")"
done
frontier="$next"
done
done
# 3. Suite mapping: every Chainsaw suite dir must resolve to a real source.
# The suite universe is auto-discovered while suite_to_source() is
# hand-maintained, so this is the part that actually drifts. A missing
# suites dir is itself a failure — otherwise a moved/misspelled path would
# make discovery yield nothing and silently pass this whole check.
if [ ! -d "$SUITES_DIR" ]; then
echo "select-install: suites dir '$SUITES_DIR' does not exist" >&2
rc=1
elif ! suites=$(discover_suites "$SUITES_DIR"); then
echo "select-install: find failed listing the suites under '$SUITES_DIR'" >&2
rc=1
else
for suite in $suites; do
src="$(suite_to_source "$suite")"
if [ -z "$src" ]; then
echo "select-install: suite '$suite' has no PackageSource mapping (add it to suite_to_source)" >&2
rc=1
elif ! echo "$NODES" | grep -Fxq "$src"; then
echo "select-install: suite '$suite' maps to '$src', not a PackageSource in $SOURCES_DIR" >&2
rc=1
fi
done
fi
[ "$rc" = 0 ] && echo "select-install: graph OK ($(echo "$NODES" | grep -c .) sources)" >&2
exit "$rc"
fi
# Closure mode: map suites to seed sources, then take the forward closure.
seeds=""
map_rc=0
for suite in $SUITES; do
[ -z "$suite" ] && continue
src="$(suite_to_source "$suite")"
case "$src" in
"")
# Fail closed: a suite that maps to nothing must abort, not silently emit
# an empty set. Collect every bad suite so one run reports them all.
echo "select-install: error: suite '$suite' has no known PackageSource mapping (add it to suite_to_source)" >&2
map_rc=1
continue ;;
esac
if echo "$NODES" | grep -Fxq "$src"; then
case " $seeds " in *" $src "*) ;; *) seeds="$seeds $src" ;; esac
else
echo "select-install: error: suite '$suite' -> '$src' is not a PackageSource in $SOURCES_DIR" >&2
map_rc=1
fi
done
[ "$map_rc" = 0 ] || exit "$map_rc"
[ -z "$seeds" ] && exit 0
# Any non-empty install set needs the engine up: it registers the
# ApplicationDefinition CRD every *-rd HelmRelease reconciles against, and its
# cozystack-api component serves the aggregated *.cozystack.io APIs some suites
# exercise (e.g. securitygroup -> sdn.cozystack.io). *-application sources carry
# a dependsOn edge to it; system-package-backed suites reach it via no edge, so
# seed it explicitly here. Its absence from the graph is a hard error — nothing
# can install without it.
engine="cozystack.cozystack-engine"
if echo "$NODES" | grep -Fxq "$engine"; then
case " $seeds " in *" $engine "*) ;; *) seeds="$seeds $engine" ;; esac
else
echo "select-install: error: required engine PackageSource '$engine' not found in $SOURCES_DIR" >&2
exit 1
fi
all="$seeds"
while :; do
new=""
for s in $all; do
for d in $(deps_of "$s"); do
case " $all $new " in *" $d "*) ;; *) new="$new $d" ;; esac
done
done
[ -z "$new" ] && break
all="$all $new"
done
echo "$all" | tr ' ' '\n' | grep -v '^$' | sort -u | paste -sd ' ' -