mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-26 21:17:32 +09:00
The Redis backup demo now takes APP_KIND=Valkey, which switches the app kind and the chart prefix the helpers derive sentinel and Secret names from. The demo BackupClass routes Valkey to the same demo strategy. The valkey suite gains valkey-backup-roundtrip, which drives that demo end to end: seed a marker, back it up, restore it in place and into a copy. It writes to tenant-root's seaweedfs through seaweedfs-s3, so a seaweedfs change now selects the valkey suite too, and since the round-trip runs examples/backups/redis as its harness, an edit there selects both the redis and the valkey suites. The reduced E2E install keeps the backup stack for the valkey suite, as it does for the other backup round-trip suites. Signed-off-by: Andrey Kolkov <androndo@gmail.com> Assisted-By: LLM
466 lines
19 KiB
Bash
Executable File
466 lines
19 KiB
Bash
Executable File
#!/bin/sh
|
|
# Given the E2E app suite(s) about to run, emit the conservative set of packages
|
|
# that must be INSTALLED for those suites: the suite owners and runtime needs,
|
|
# the E2E install baseline, and their forward PackageSource dependency closure.
|
|
# 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 --disabled <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 platform baseline + all transitive
|
|
# dependsOn. Empty when <suites> is empty.
|
|
#
|
|
# Output (--disabled): the COMPLEMENT of that closure, for bundles.disabledPackages
|
|
# in the platform chart. The base package helper in
|
|
# packages/core/platform/templates/_helpers.tpl consults disabledPackages for
|
|
# every package rather than only the optional ones, so an ordinary variant
|
|
# can be reduced by subtraction with no new variant. Refuses on an empty
|
|
# selection, where the complement would be the whole platform.
|
|
#
|
|
# Three limits worth knowing before consuming it.
|
|
#
|
|
# Subtraction governs a FRESH install only. Both package defines stamp
|
|
# `helm.sh/resource-policy: keep`, so adding a name on a live cluster stops
|
|
# emitting the Package CR but does not remove the one already there; the
|
|
# package stays installed. Fine for a per-run E2E install, wrong as a way to
|
|
# uninstall.
|
|
#
|
|
# Packages emitted through package.optional (nfs-driver, telepresence,
|
|
# external-dns, external-dns-application, and the others gated the same way)
|
|
# need bundles.enabledPackages as well. Keeping one in the closure is
|
|
# therefore not enough to install it, and a suite whose closure contains one
|
|
# needs that list set separately. Subtraction is correct for them either way,
|
|
# since an optional package absent from enabledPackages is not installed
|
|
# regardless.
|
|
#
|
|
# A package that no selected suite owns or needs, and that the baseline does
|
|
# not keep, is not installed, including when select-e2e.sh answers with every
|
|
# suite. That is deliberate: nothing in the run looks at such packages
|
|
# (monitoring exporters like goldpinger and kubeovn-plunger, apps without a
|
|
# suite, provider-specific operators), so installing them only adds a
|
|
# readiness wait. A change to one of them gets no install check from this gate.
|
|
#
|
|
# The declared graph is not the whole runtime contract. The standard E2E
|
|
# install waits for storage, load-balancer, CAPI and OIDC components which no
|
|
# selected application necessarily reaches, so they are explicit baseline
|
|
# seeds below. This mode describes a FRESH install using that audited path;
|
|
# live convergence still has to be proved separately. 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> has real PackageSources for both its
|
|
# owners and runtime requirements. Exit non-zero on any failure. The graph
|
|
# is read from the current PackageSource inventory. The hand-maintained suite
|
|
# mapping can drift independently, so validation checks it against both that
|
|
# inventory and the current suite directories.
|
|
#
|
|
# 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:-}" = "--disabled" ]; then
|
|
MODE="disabled"
|
|
shift
|
|
suites_arg="${1?usage: select-install.sh --disabled <suites> [sources-dir]}"
|
|
SOURCES_DIR="${2:-$SOURCES_DIR}"
|
|
if [ "$suites_arg" = "-" ]; then
|
|
SUITES="$(cat)"
|
|
else
|
|
SUITES="$suites_arg"
|
|
fi
|
|
elif [ "${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(s). Prints:
|
|
# - one or more "cozystack.*" names every direct 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.
|
|
#
|
|
# A suite may have more than one direct owner. The Kubernetes suites create
|
|
# Kubernetes and KubernetesNodes; vminstance creates VMInstance and VMDisk and
|
|
# observes the DataVolume behind the disk. Each API needs its application source.
|
|
# Fixtures which only name other app kinds as selectors are not owners.
|
|
suite_to_source() {
|
|
case "$1" in
|
|
kubernetes-latest|kubernetes-previous)
|
|
echo "cozystack.kubernetes-application cozystack.kubernetes-nodes-application"
|
|
return ;;
|
|
vminstance)
|
|
echo "cozystack.vm-instance-application cozystack.vm-disk-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
|
|
}
|
|
|
|
# Backup examples create Bucket and backup CRs alongside the selected app;
|
|
# gateway's namespace-churn test also requires the pod admission webhook.
|
|
# These requirements belong to the suites; adding application graph edges
|
|
# would also change reverse test selection.
|
|
suite_runtime_sources() {
|
|
case "$1" in
|
|
clickhouse|etcd|kafka|kafka-metadata|mariadb|mongodb|postgres|rabbitmq|redis|valkey)
|
|
echo cozystack.backupstrategy-controller ;;
|
|
gateway) echo cozystack.kubeovn-webhook ;;
|
|
esac
|
|
}
|
|
|
|
# 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
|
|
}
|
|
|
|
# Capture each producer before sorting. This file runs under dash through
|
|
# cozytest, so pipefail is not available; checking the producer explicitly is
|
|
# what prevents a failed yq from turning into a successful empty graph.
|
|
if ! nodes_raw=$(yq -rN '.metadata.name | select(. != null and . != "")' "$SOURCES_DIR"/*.yaml); then
|
|
echo "select-install: yq failed while reading PackageSource names from $SOURCES_DIR" >&2
|
|
exit 1
|
|
fi
|
|
NODES=$(printf '%s\n' "$nodes_raw" | sort -u)
|
|
if [ -z "$NODES" ]; then
|
|
echo "select-install: found no PackageSources in $SOURCES_DIR" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! forward_raw=$(build_forward_deps); then
|
|
echo "select-install: yq failed while reading PackageSource dependencies from $SOURCES_DIR" >&2
|
|
exit 1
|
|
fi
|
|
FORWARD=$(printf '%s\n' "$forward_raw" | sort -u)
|
|
|
|
# forward deps of a single node
|
|
deps_of() {
|
|
echo "$FORWARD" | awk -v s="$1" -F'\t' '$1==s {print $2}'
|
|
}
|
|
|
|
# Validate the complete PackageSource graph, including branches unrelated to
|
|
# the requested suites. Any emitted closure or complement describes this
|
|
# inventory as a whole, so accepting a dangling edge or cycle elsewhere would
|
|
# make the result depend on an invalid input snapshot.
|
|
validate_graph() {
|
|
# One awk pass checks every reference and walks every node once, because
|
|
# ordinary selection runs this validation too and the graph has ~100 nodes.
|
|
# The node list goes through the environment: BWK awk rejects literal
|
|
# newlines in -v assignments.
|
|
if graph_errors=$(printf '%s\n' "$FORWARD" | SELECT_INSTALL_NODES="$NODES" awk -F'\t' '
|
|
function visit(node, i, next_node) {
|
|
if (state[node] == 1) {
|
|
cycle = node
|
|
return 1
|
|
}
|
|
if (state[node] == 2)
|
|
return 0
|
|
state[node] = 1
|
|
for (i = 1; i <= degree[node]; i++) {
|
|
next_node = edge[node, i]
|
|
if (visit(next_node))
|
|
return 1
|
|
}
|
|
state[node] = 2
|
|
return 0
|
|
}
|
|
BEGIN {
|
|
count = split(ENVIRON["SELECT_INSTALL_NODES"], known_list, "\n")
|
|
for (i = 1; i <= count; i++)
|
|
known[known_list[i]] = 1
|
|
}
|
|
NF == 2 {
|
|
if (references[$2] == "")
|
|
references[$2] = $1
|
|
else
|
|
references[$2] = references[$2] "," $1
|
|
edge[$1, ++degree[$1]] = $2
|
|
}
|
|
END {
|
|
rc = 0
|
|
for (dependency in references) {
|
|
if (!(dependency in known)) {
|
|
print "select-install: dangling dependency \047" dependency \
|
|
"\047 (referenced by: " references[dependency] ") has no PackageSource"
|
|
rc = 1
|
|
}
|
|
}
|
|
for (i = 1; i <= count; i++) {
|
|
if (visit(known_list[i])) {
|
|
print "select-install: dependency cycle detected involving \047" cycle "\047"
|
|
rc = 1
|
|
break
|
|
}
|
|
}
|
|
exit rc
|
|
}
|
|
'); then
|
|
return 0
|
|
fi
|
|
printf '%s\n' "$graph_errors" >&2
|
|
return 1
|
|
}
|
|
|
|
# all Chainsaw suite names under a suites-dir (top-level dirs holding, in them or
|
|
# below, chainsaw-test.yaml, chainsaw-test.yml or numbered step files such as
|
|
# 01-install.yaml), discovered exactly like select-e2e.sh. find runs from inside
|
|
# the dir so the suite name is the first path component however the dir is
|
|
# spelled. It 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() {
|
|
if ! found=$(cd "$1" && find . -mindepth 2 \( ! -type d -o -name chainsaw-test.yaml -o -name chainsaw-test.yml \)); then
|
|
echo "select-install: find failed listing the suites under $1" >&2
|
|
return 1
|
|
fi
|
|
suites=$(printf '%s\n' "$found" \
|
|
| sed -nE 's,^\./([^/]+)/(.*/)?(chainsaw-test|[0-9]+-[^/]*)\.ya?ml$,\1,p' | sort -u)
|
|
if [ -z "$suites" ]; then
|
|
echo "select-install: found no chainsaw-test.yaml, chainsaw-test.yml or numbered step file under $1" >&2
|
|
return 1
|
|
fi
|
|
printf '%s\n' "$suites"
|
|
}
|
|
|
|
if [ "$MODE" = "validate" ]; then
|
|
rc=0
|
|
|
|
validate_graph || rc=1
|
|
|
|
# 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
|
|
rc=1
|
|
else
|
|
for suite in $suites; do
|
|
owners="$(suite_to_source "$suite")"
|
|
if [ -z "$owners" ]; then
|
|
echo "select-install: suite '$suite' has no PackageSource mapping (add it to suite_to_source)" >&2
|
|
rc=1
|
|
continue
|
|
fi
|
|
for src in $owners $(suite_runtime_sources "$suite"); do
|
|
if ! 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
|
|
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
|
|
owners="$(suite_to_source "$suite")"
|
|
if [ -z "$owners" ]; then
|
|
# 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
|
|
fi
|
|
for src in $owners $(suite_runtime_sources "$suite"); do
|
|
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
|
|
done
|
|
[ "$map_rc" = 0 ] || exit "$map_rc"
|
|
|
|
# An empty seed set means no suite was selected. In closure mode that is a valid
|
|
# answer -- install nothing extra. In disabled mode it is not: the complement of
|
|
# an empty closure is the whole platform, which as an install instruction is a
|
|
# platform with no packages at all, so refuse rather than emit it.
|
|
if [ -z "$seeds" ]; then
|
|
if [ "$MODE" = "disabled" ]; then
|
|
echo "select-install: refusing to emit a disable list for an empty suite selection — the complement would be the entire platform" >&2
|
|
exit 1
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# Do not emit an answer from an invalid inventory. Suite-directory validation
|
|
# remains exclusive to --validate because ordinary selection must not depend on
|
|
# scanning an unrelated test tree.
|
|
validate_graph || exit 1
|
|
|
|
# The standard E2E install has unconditional runtime requirements which are not
|
|
# owned by any selected application. Seed those direct requirements here rather
|
|
# than inventing PackageSource edges: graph edges also drive select-e2e.sh's
|
|
# reverse test selection, while these dependencies belong to the install harness.
|
|
#
|
|
# Root platform: aggregated APIs, tenant namespaces and three root services
|
|
# (etcd, ingress, SeaweedFS) are waited before a selected suite can finish;
|
|
# monitoring is not waited and stays only because tenant-application depends on
|
|
# it. Storage/network prep waits
|
|
# LINSTOR, CDI and MetalLB. The install then waits the CAPI operator plus all four
|
|
# providers, and later enables and waits the Keycloak OIDC stack. Kamaji,
|
|
# KubeVirt, storage controllers and CRDs remain graph-derived so losing those
|
|
# declared edges stays observable in tests. Tenant HelmReleases carry a shard
|
|
# label excluded by flux-aio, so the shard operator must supply their reconciler.
|
|
#
|
|
# Platform services no suite owns are kept as well, so a selective install still
|
|
# runs the platform the way a full one does: multus (secondary networks), the
|
|
# cozystack and LINSTOR schedulers, flux-plunger (repairs HelmReleases stuck on
|
|
# "has no deployed releases"), cozy-proxy (external Services of VMs) and
|
|
# ouroboros (hairpin NAT, asserted by the Kubernetes suites). Nothing waits for
|
|
# them, so no graph edge reaches them; keeping them here leaves the product
|
|
# PackageSources untouched.
|
|
baseline="cozystack.cozystack-engine
|
|
cozystack.flux-shard-operator
|
|
cozystack.cozystack-basics
|
|
cozystack.tenant-application
|
|
cozystack.etcd-application
|
|
cozystack.ingress-application
|
|
cozystack.monitoring-application
|
|
cozystack.seaweedfs-application
|
|
cozystack.linstor
|
|
cozystack.kubevirt-cdi
|
|
cozystack.metallb
|
|
cozystack.capi-operator
|
|
cozystack.capi-provider-bootstrap-kubeadm
|
|
cozystack.capi-provider-core
|
|
cozystack.capi-provider-cp-kamaji
|
|
cozystack.capi-provider-infra-kubevirt
|
|
cozystack.keycloak
|
|
cozystack.keycloak-operator
|
|
cozystack.multus
|
|
cozystack.cozystack-scheduler
|
|
cozystack.linstor-scheduler
|
|
cozystack.flux-plunger
|
|
cozystack.cozy-proxy
|
|
cozystack.ouroboros"
|
|
|
|
missing=""
|
|
for b in $baseline; do
|
|
if echo "$NODES" | grep -Fxq "$b"; then
|
|
case " $seeds " in *" $b "*) ;; *) seeds="$seeds $b" ;; esac
|
|
else
|
|
missing="${missing:+$missing }$b"
|
|
fi
|
|
done
|
|
if [ -n "$missing" ]; then
|
|
echo "select-install: error: required baseline PackageSource(s) not found in $SOURCES_DIR: $missing" >&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
|
|
|
|
closure="$(echo "$all" | tr ' ' '\n' | grep -v '^$' | sort -u)"
|
|
|
|
# These runtime prerequisites are intentionally graph-derived rather than
|
|
# redundant seeds. That keeps their PackageSource edges meaningful: if a CAPI
|
|
# provider loses Kamaji or KubeVirt, or LINSTOR loses one of its controllers,
|
|
# the selector fails closed instead of silently emitting an under-sized install.
|
|
derived_baseline="cozystack.cert-manager
|
|
cozystack.networking
|
|
cozystack.gateway-api-crds
|
|
cozystack.etcd-operator
|
|
cozystack.grafana-operator
|
|
cozystack.postgres-operator
|
|
cozystack.victoria-metrics-operator
|
|
cozystack.prometheus-operator-crds
|
|
cozystack.vertical-pod-autoscaler
|
|
cozystack.reloader
|
|
cozystack.snapshot-controller
|
|
cozystack.objectstorage-controller
|
|
cozystack.gateway-application
|
|
cozystack.info-application
|
|
cozystack.kamaji
|
|
cozystack.kubevirt"
|
|
missing_sources=""
|
|
missing_reachable=""
|
|
for required in $derived_baseline; do
|
|
if ! echo "$NODES" | grep -Fxq "$required"; then
|
|
missing_sources="${missing_sources:+$missing_sources }$required"
|
|
elif ! echo "$closure" | grep -Fxq "$required"; then
|
|
missing_reachable="${missing_reachable:+$missing_reachable }$required"
|
|
fi
|
|
done
|
|
if [ -n "$missing_sources" ]; then
|
|
echo "select-install: error: required runtime PackageSource(s) not found in $SOURCES_DIR: $missing_sources" >&2
|
|
exit 1
|
|
fi
|
|
if [ -n "$missing_reachable" ]; then
|
|
echo "select-install: error: runtime baseline dependency closure is missing: $missing_reachable" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ "$MODE" = "disabled" ]; then
|
|
# The complement is every PackageSource minus the audited baseline, selected
|
|
# owners and their declared forward closure. No declared dependency of a kept
|
|
# package can enter it. The explicit baseline covers the current install
|
|
# harness's known undeclared requirements; only a fresh live run can establish
|
|
# that no other operational dependency is missing.
|
|
echo "$NODES" | grep -vxF "$closure" | paste -sd ' ' -
|
|
exit 0
|
|
fi
|
|
|
|
echo "$closure" | paste -sd ' ' -
|