mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-03 10:37:21 +09:00
## What this PR does Gives the ClickHouse `clickhouse-backup` sidecar a way to trust an S3 endpoint whose certificate is signed by a private CA, and uses that to make the ClickHouse backup/restore e2e round-trip actually run in CI. This actualizes #2600, whose feature code and example scripts have since landed on `main` independently, and whose remaining contribution — the e2e test — was still in the retired BATS format. #2600 can be closed once this merges. ### 1. `backup.endpointCA` on the ClickHouse chart The sidecar had **no TLS/CA surface at all**: bucket coordinates and credentials arrive as `S3_*` env, and nothing let it verify an endpoint signed by a private CA. Cozystack's own in-cluster SeaweedFS is exactly that case — it serves `:8333` behind the self-signed "SeaweedFS CA" — so a ClickHouse release could only ever back up to a publicly-trusted endpoint. MariaDB (`storage.s3.tls.caSecretKeyRef`) and Postgres (barman `endpointCA`) have had a CA field all along; this closes the gap for ClickHouse. `backup.endpointCA` takes `{name, key}`. When set, the chart mounts that Secret into the sidecar and points `SSL_CERT_DIR` at it. Empty — the default — renders no env, no mount and no volume, so existing releases are byte-for-byte unchanged. **Why `SSL_CERT_DIR` and not `AWS_CA_BUNDLE`.** Both are honoured by `altinity/clickhouse-backup:2.7.4`; I measured them against a self-signed endpoint and public AWS S3 in turn. Go reads `SSL_CERT_DIR` *in addition to* the system bundle, so both endpoints verify. `AWS_CA_BUNDLE` *replaces* the SDK's trust pool: the private endpoint verifies, and public S3 then fails `x509: certificate signed by unknown authority` — it would silently break a release that also talks to a public endpoint. `S3_DISABLE_CERT_VERIFICATION` was rejected outright, since it drops verification rather than trusting one more CA. ### 2. The e2e suite, and what it was really testing Two Tests added to the existing `hack/e2e-chainsaw/clickhouse/` suite (in name order, mirroring the `etcd`/`mariadb`/`postgres` shape — see the layout rationale below for why they live here rather than in a new directory): - **`clickhouse-1-backup-contracts`** (CI, `tenant-test`, ~98s): a backup-enabled ClickHouse asserted against the *running* cluster — the `clickhouse-backup` sidecar on the `ch-backup-api` `:7171` port, and the `<release>-backup-api-auth` Secret whose keys the sidecar's `API_USERNAME`/`API_PASSWORD` actually resolve `valueFrom`. Each is a silent break if a chart bump renames it. Needs no object store. - **`clickhouse-2-backup-roundtrip`** (CI, `tenant-root`): `Bucket` → `Altinity` strategy + `BackupClass` → source ClickHouse with a sentinel row → `BackupJob` → in-place `RestoreJob` → to-copy `RestoreJob` into a second instance, driving `examples/backups/clickhouse/run-all.sh` so test and docs cannot drift. The round-trip was originally gated behind `CLICKHOUSE_E2E_S3_ROUNDTRIP` and **skipped in CI** — its `PASS (1.01s)` meant the gate fired, not that a backup happened. `backup.endpointCA` removes the reason for the gate, so the suite now adopts the same three preconditions mariadb/postgres rely on: run in `tenant-root` (an isolated tenant's Cilium egress allowlist blocks its Pods from reaching `tenant-root`'s SeaweedFS), target the in-cluster endpoint instead of the `Bucket`'s external ingress URL (an unroutable placeholder in CI), and trust its certificate via the copied CA. It also pre-cleans with `cleanup.sh`, because `tenant-root` is shared and a leftover `Succeeded` `BackupJob` would satisfy the harness's wait without a backup ever running. Both backup Tests live in the existing `hack/e2e-chainsaw/clickhouse/` suite rather than a new suite directory, so no selector mapping is added: `hack/select-e2e.sh` already derives `clickhouse` both from `packages/apps/clickhouse/**` (the `*-application` rule) and from `examples/backups/clickhouse/**` (the harness path), and both paths therefore select the suite that actually runs the harness. That alignment is the reason for the layout — a separate `clickhouse-backup/` directory would have made an edit to the harness select a suite that cannot exercise it. `hack/select-e2e.sh` and `hack/select-install.sh` are otherwise untouched (the only change is a comment recording that invariant), and the existing `backup example harness edit selects its app suite` case in `hack/select-e2e_test.bats` covers the mapping. ### Verification - `helm unittest` — 17 pass, including 5 new `endpointCA` cases: the unset default renders nothing, the env/mount/volume wiring, a custom key still projected as `ca.crt`, composition with `useSystemBucket`, and `backup.enabled=false` renders nothing. - The CA-trust behaviour was measured against the real image, not assumed: without a CA knob the sidecar reports `x509: certificate signed by unknown authority`; with `SSL_CERT_DIR` the handshake succeeds. - `hack/select-e2e_test.bats` (16) and `hack/select-install_test.bats` (17) pass; `select-install.sh --validate` reports graph OK. - `make generate` is idempotent, so the pre-commit job's `git diff --exit-code` stays clean. - The contracts Test passed in CI on earlier runs of this branch. The round-trip only became CI-live with this work, so it still needs a green E2E run before merge — the branch has since been rebased onto current `main` to get one. ### Screenshots Not applicable — no UI changes. ### Downstream repositories Walked the trigger map against the diff, file by file. The load-bearing change is the new `backup.endpointCA` field in `packages/apps/clickhouse/values.yaml` / `values.schema.json`; everything else is `hack/e2e-chainsaw/`, `examples/backups/`, one docs paragraph, and regenerated artifacts, none of which appear in the map. - [ ] No downstream repository is affected by this change - [x] [cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack) - follow-up: https://github.com/cozystack/terraform-provider-cozystack/issues/18 — "add, remove or rename a field in an app's `values.schema.json`" reaches the provider's hand-maintained schema, model and expand/flatten pair, so the new nested object is not surfaced until it is added there. Opened as an issue rather than a PR because the natural moment is alongside that repo's next `api/apps/v1alpha1` pin bump, which is a maintainer call. Not ticked, with reasons rather than silence: **cozystack/website** — the trigger is *adding, renaming or removing a package*, and this adds a field to an existing one. `clickhouse` is already in the website `Makefile`'s app list and its reference page is generated from this package's `README.md`, so the regenerated `backup.endpointCA` rows are carried by the existing release-tag automation with no manual PR. **cozystack/external-apps-example** — vendors `hack/package.mk` and `update-crd.sh`, neither of which this PR touches. **cozystack/ansible-cozystack**, **ccp**, **talm**, **cozyhr**, **cozy-proxy**, **cozystack-telemetry-server**, **examples** — no installer values, `hack/` layout, namespace, variant, node-prerequisite, annotation, label or metric in their trigger rows is touched here. ### Release note ```release-note feat(clickhouse): add `backup.endpointCA` so the clickhouse-backup sidecar can verify an S3 endpoint whose certificate is signed by a private CA, such as Cozystack's in-cluster SeaweedFS. Leave it unset for publicly-trusted endpoints; existing releases are unaffected. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added optional private S3 endpoint CA support for ClickHouse backup/restore sidecars via `backup.endpointCA`, including configurable Secret name and CA bundle key (default `ca.crt`). * Sidecars mount the CA and extend trust while leaving public endpoints unchanged. * **Documentation** * Updated ClickHouse chart docs, operational backup guidance, and backup/restore examples to cover CA setup, overrides, and cleanup. * **Tests** * Added helm-unittest and end-to-end coverage validating CA rendering/mounting behavior and ClickHouse backup/restore workflows. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
418 lines
21 KiB
Bash
Executable File
418 lines
21 KiB
Bash
Executable File
#!/bin/sh
|
|
# Read a list of changed files (one per line) and emit space-separated suite
|
|
# names whose Chainsaw suites under hack/e2e-chainsaw/ should run.
|
|
#
|
|
# Usage: hack/select-e2e.sh <changed-files> [<sources-dir>]
|
|
# Defaults: sources-dir = packages/core/platform/sources
|
|
#
|
|
# Output:
|
|
# - empty no E2E impact, decided by one of the rules that select
|
|
# nothing: step 1 for docs/, dashboards/ and *.md, step 2 for
|
|
# a *.disabled Chainsaw suite and for an
|
|
# examples/backups/<app>/ with no suite, and
|
|
# inert_config_pattern for repo meta and agent config
|
|
# - <suite names> selected per the PackageSource dependency graph
|
|
# - full list any path that affects all tests, OR an unrecognised
|
|
# packages/* path, OR a changed package that reaches no
|
|
# runnable suite through the graph, OR a path matching NEITHER
|
|
# the full-suite nor the inert list, OR a yq that failed to
|
|
# build the dependency graph (conservative fallbacks)
|
|
# - nothing, and the suite list itself is unavailable: find failed, or
|
|
# a non-zero hack/e2e-chainsaw holds no chainsaw-test.yaml. Unlike the
|
|
# exit yq case there is no fallback left to take — an empty list
|
|
# silently corrupts both the escalations, which would print
|
|
# it, and the backups rule, which membership-tests against
|
|
# it — so the script refuses to answer at all, and since both
|
|
# lanes run it under `bash -e` the step fails
|
|
#
|
|
# Every changed path must land in exactly one of the three selection classes by
|
|
# an explicit rule; the fourth outcome is not a classification but a refusal to
|
|
# produce one. An unclassified path escalates to the full suite rather than
|
|
# selecting nothing: both e2e lanes read an empty selection as "skip Chainsaw"
|
|
# and then post the required "E2E Tests" status green, so a silent default is a
|
|
# green gate with no suite run (#3392). When that escalation fires for a path
|
|
# that genuinely cannot affect e2e, add it to inert_config_pattern — do not
|
|
# widen the fall-through.
|
|
#
|
|
# "Empty" is not a synonym for inert_config_pattern. Several rules select
|
|
# nothing: step 1 for docs/, dashboards/ and *.md, step 2 for a *.disabled
|
|
# Chainsaw suite and for an examples/backups/<app>/ whose app has no suite, and
|
|
# the pattern itself. The property the lanes depend on is weaker than "matched
|
|
# the inert list" and it is the one to preserve: an empty selection is a
|
|
# decision some rule reached, never a path nothing looked at.
|
|
#
|
|
# One caveat worth knowing before trusting that: inert_config_pattern makes
|
|
# .github/ inert as a whole directory, and the e2e workflows are exempted by
|
|
# NAME in full_suite_pattern, which is checked first. So a workflow added under
|
|
# .github/workflows/ that runs the suite is silently inert until someone adds it
|
|
# to that list — the fall-through never sees it. The enumeration is the guard,
|
|
# which is why it carries the `rg -l test-chainsaw` reminder; treat that comment
|
|
# as load-bearing rather than decorative.
|
|
set -eu
|
|
|
|
CHANGED="${1:?missing changed-files arg}"
|
|
SOURCES_DIR="${2:-packages/core/platform/sources}"
|
|
|
|
# Anything matching this pattern triggers the full Chainsaw suite. Per-suite
|
|
# edits (hack/e2e-chainsaw/<name>/...) are matched BEFORE this so editing one
|
|
# suite doesn't escalate to the full suite; the shared _lib helpers and the
|
|
# .chainsaw.yaml config affect every suite and DO escalate (handled inline).
|
|
#
|
|
# Three groups, all "cannot be scoped to one app":
|
|
# - shipped code and its build inputs: packages/library, packages/core, the Go
|
|
# trees (api, cmd, internal, pkg), the codegen under tools/, go.mod/go.sum,
|
|
# the root Makefile, hack/*.mk (the tag/push/output flags of every image),
|
|
# hack/buildkitd.toml (the builder every image is built with), and hack/lib/
|
|
# (sourced by the hack/*.sh scripts that already escalate);
|
|
# - the e2e harness itself: hack/*.sh, hack/*.bats and hack/e2e-*.yaml;
|
|
# - the workflows that RUN the suite — enumerated rather than matched by
|
|
# prefix, so an unrelated workflow does not burn a full run. Keep this list
|
|
# in step with `rg -l test-chainsaw .github/workflows/`.
|
|
#
|
|
# What that costs, stated rather than left to be rediscovered: only
|
|
# pull-requests.yaml is executed from the PR's own head. e2e-fork runs on
|
|
# workflow_run, which always takes the default-branch copy; e2e-tag runs on
|
|
# workflow_call from a tag push; nightly runs on a schedule. So for those
|
|
# three the suite cannot exercise the edit under review, and escalating buys
|
|
# generic regression coverage rather than coverage of the change. Measured
|
|
# over the last six months of main: 26 commits touch one of the three with
|
|
# no other escalating path, and each now pays a full Chainsaw run it would
|
|
# previously have skipped. They stay on the list so the rule remains one
|
|
# idea ("workflows that run the suite") rather than two, and because the
|
|
# alternative leaves them inert, which reads as an oversight rather than a
|
|
# decision. Reopen the trade if the full suite's flake rate makes the
|
|
# generic coverage cost more than it returns.
|
|
full_suite_pattern='^(packages/library/|packages/core/|api/|cmd/|internal/|pkg/|tools/|hack/lib/|hack/[^/]+\.sh$|hack/[^/]+\.bats$|hack/[^/]+\.mk$|hack/buildkitd\.toml$|hack/e2e-[^/]+\.ya?ml$|go\.(mod|sum)$|Makefile$|\.github/workflows/(pull-requests|e2e-fork|e2e-tag|nightly)\.yaml$)'
|
|
|
|
# Paths with no bearing on what e2e exercises. Checked AFTER full_suite_pattern,
|
|
# so a specific escalation wins over a broad directory here (.github/ is inert,
|
|
# .github/workflows/pull-requests.yaml is not). This list is the whole reason
|
|
# the fall-through at the bottom of the loop can escalate safely: the cost of
|
|
# forgetting an inert path is a wasted full run, the cost of forgetting a live
|
|
# one used to be a green gate with nothing tested.
|
|
# - examples/ demo manifests; examples/backups/<app>/ is handled above
|
|
# as a real test harness before this check is reached
|
|
# - .github/ templates, CODEOWNERS, labels, renovate, linter config —
|
|
# minus the e2e workflows escalated above
|
|
# - .claude/ .gemini/ agent config, never shipped
|
|
# - img/ README assets
|
|
# - hack/testdata/ fixtures for the bats unit tests, not for e2e
|
|
# - boilerplate.go.txt / dcgm-default-counters.csv codegen header; a CSV read
|
|
# only by check-gpu-recording-rules.bats
|
|
inert_config_pattern='^(examples/|\.github/|\.claude/|\.gemini/|img/|hack/testdata/|hack/boilerplate\.go\.txt$|hack/dcgm-default-counters\.csv$|LICENSE$|\.gitignore$|\.pre-commit-config\.yaml$|\.coderabbit\.yaml$)'
|
|
|
|
# All known Chainsaw suites: every dir under hack/e2e-chainsaw/ holding a
|
|
# chainsaw-test.yaml (this excludes _lib/ and the top-level config files).
|
|
#
|
|
# Captured before the sed/sort rather than piped straight into them: a pipeline
|
|
# carries its LAST command's status, so `$(find ... | sed | sort)` would report
|
|
# sort's success whatever find did — the same blindness handled for yq below,
|
|
# and worse here, because this list is what every escalation prints. Errors go
|
|
# to stderr rather than /dev/null for the same reason.
|
|
if ! chainsaw_tests=$(find hack/e2e-chainsaw -mindepth 2 -maxdepth 2 -name chainsaw-test.yaml); then
|
|
echo "select-e2e: find failed listing the Chainsaw suites under hack/e2e-chainsaw — nothing can be decided without that list" >&2
|
|
exit 1
|
|
fi
|
|
all_apps=$(printf '%s\n' "$chainsaw_tests" \
|
|
| sed -e 's,^hack/e2e-chainsaw/,,' -e 's,/chainsaw-test\.yaml$,,' | sort)
|
|
|
|
# An empty list here is a broken enumeration — a moved directory, a wrong
|
|
# working directory — not a project without tests, and it silently corrupts
|
|
# every answer the script can give:
|
|
#
|
|
# - the three escalation branches print this list, so "run everything"
|
|
# becomes a blank line, which both lanes read as "skip Chainsaw" before
|
|
# posting the required "E2E Tests" status green;
|
|
# - the examples/backups/<app>/ rule takes no escalation and still consults
|
|
# it, as a membership test. With the list empty the test fails, the app is
|
|
# not selected, and an edit to a backup harness that should run its suite
|
|
# reports nothing to run instead.
|
|
#
|
|
# Neither is distinguishable in the output from a legitimately empty selection,
|
|
# which is the failure #3392 exists to keep out. Checking once here rather than
|
|
# at the escalation branches covers both, and covers the next consumer that
|
|
# reads the list without escalating.
|
|
#
|
|
# Both lanes run this step under `bash -e`, so the non-zero exit fails the job
|
|
# instead of falling through to the empty selection.
|
|
if [ -z "$all_apps" ]; then
|
|
echo "select-e2e: found no chainsaw-test.yaml under hack/e2e-chainsaw — the suite enumeration is broken (wrong working directory?), refusing to decide anything from an empty suite list" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Single exit point for the three escalating branches, so the invariant above
|
|
# has one consumer to reason about rather than three copies.
|
|
escalate_to_full_suite() {
|
|
echo "$all_apps" | paste -sd ' ' -
|
|
exit 0
|
|
}
|
|
|
|
# PackageSource name -> Chainsaw suite name(s). Most *-application sources map
|
|
# by stripping the suffix, a source that is not an app carries its own name
|
|
# (cozystack.kuberture owns the kuberture suite), and the few that match neither
|
|
# are listed explicitly.
|
|
#
|
|
# This is the inverse of select-install.sh's suite_to_source() and has to stay
|
|
# in step with it — a suite that does not round-trip is unreachable from its own
|
|
# package, so every change to that package escalates to the full run instead of
|
|
# selecting the one suite that covers it (#3665). The round-trip is pinned by a
|
|
# test rather than left to review.
|
|
#
|
|
# Mapping optimistically is safe: a name that is not a suite is dropped by
|
|
# intersect_suites() downstream.
|
|
#
|
|
# Only names the two general arms get wrong are listed. `postgres-application`
|
|
# and `external-dns` used to sit here and no longer do: the first is what
|
|
# stripping the suffix already produces, and the second is what carrying the
|
|
# source name through already produces, so both said the rule twice and made
|
|
# this list read as longer than the set of real exceptions.
|
|
src_to_suites() {
|
|
case "$1" in
|
|
vm-instance-application) echo vminstance ;;
|
|
kubernetes-application) echo "kubernetes-latest kubernetes-previous kubernetes-oidc-system kubernetes-oidc-customconfig" ;;
|
|
securitygroup-controller) echo securitygroup ;;
|
|
*-application) echo "${1%-application}" ;;
|
|
*) echo "$1" ;;
|
|
esac
|
|
}
|
|
|
|
# yq: path -> PackageSource name
|
|
build_owners_index() {
|
|
yq -rN '.metadata.name as $n | .spec.variants[]?.components[]?.path | select(. != null and . != "") | . + "\t" + $n' "$SOURCES_DIR"/*.yaml
|
|
}
|
|
|
|
# yq: PackageSource name -> sources that depend on it (reverse of dependsOn)
|
|
#
|
|
# Exclude cozystack.cozystack-engine as a propagation hub. Every *-application
|
|
# source declares `dependsOn: cozystack.cozystack-engine` purely as an INSTALL
|
|
# ORDERING edge — the app's *-rd HelmRelease must wait for the engine to
|
|
# register the ApplicationDefinition CRD before it can reconcile. That is a
|
|
# universal lifecycle dependency, NOT a behavioral one: a change to an engine
|
|
# dependency (postgres-operator, keycloak, cert-manager, ...) does not alter any
|
|
# app's runtime behavior, so it must not fan test selection out to every app.
|
|
# Without this filter, postgres-operator -> keycloak -> cozystack-engine -> EVERY
|
|
# app, defeating test-impact analysis. Dropping the engine reverse edges keeps
|
|
# the engine reachable but stops it propagating selection downstream. A change to
|
|
# the engine itself still triggers the full suite: its group reaches no suite, so
|
|
# the per-path escalation below fires on it.
|
|
build_reverse_deps() {
|
|
yq -rN '.metadata.name as $n | .spec.variants[]?.dependsOn[]? | select(. != null and . != "" and . != "cozystack.cozystack-engine") | . + "\t" + $n' "$SOURCES_DIR"/*.yaml
|
|
}
|
|
|
|
# Each index is one yq, and `OWNERS=$(build_owners_index | sort -u)` reports
|
|
# sort's status, never yq's — so `set -eu` above cannot see a yq that failed.
|
|
# The index then comes back empty, every path falls through to escalation, and
|
|
# the run is a full suite indistinguishable in the output from "no owners
|
|
# matched". Safe in direction, invisible in nature: selection has stopped
|
|
# working and it reads as the conservatism it is supposed to be.
|
|
#
|
|
# So capture each half on its own, check its status, and say once, loudly, that
|
|
# yq is the broken component before taking the same fallback the empty index
|
|
# would have taken anyway.
|
|
broken=''
|
|
OWNERS=$(build_owners_index) || broken='the ownership index'
|
|
REVERSE=$(build_reverse_deps) || broken="${broken:+$broken and }the reverse-dependency index"
|
|
if [ -n "$broken" ]; then
|
|
echo "select-e2e: yq failed building $broken from $SOURCES_DIR/*.yaml — the dependency graph is unusable, running the full suite" >&2
|
|
escalate_to_full_suite
|
|
fi
|
|
# Sorted after the check rather than inside the pipeline above, for the reason
|
|
# the pipeline was the bug.
|
|
OWNERS=$(printf '%s\n' "$OWNERS" | sort -u)
|
|
REVERSE=$(printf '%s\n' "$REVERSE" | sort -u)
|
|
|
|
trigger_full=0
|
|
trigger_any=0
|
|
selected_groups=""
|
|
selected_apps=""
|
|
|
|
# `|| [ -n "$file" ]` is what makes the classification below total. POSIX read
|
|
# assigns the last line and then returns non-zero when the input does not end in
|
|
# a newline, so a plain `while read` silently drops it. That is the one way a
|
|
# path can reach this loop and be classified by nothing at all: the fall-through
|
|
# at the end, which exists so an unrecognised path escalates instead of
|
|
# selecting nothing, sits inside the body the drop skips. Callers building the
|
|
# list themselves rather than piping `git diff` are the ones that hit it, and
|
|
# the guard belongs here rather than in each of them.
|
|
while IFS= read -r file || [ -n "$file" ]; do
|
|
[ -z "$file" ] && continue
|
|
|
|
# 1. Skip: docs, dashboards, *.md. Checked before everything else because
|
|
# these can never matter wherever they live — a README inside
|
|
# packages/core/ must not escalate the way its templates do.
|
|
if echo "$file" | grep -qE '^(docs/|dashboards/)' || echo "$file" | grep -qE '\.md$'; then
|
|
continue
|
|
fi
|
|
|
|
# 2. Chainsaw edits. A per-suite file selects only that suite; the shared
|
|
# _lib helpers and the .chainsaw.yaml config affect every suite, so they
|
|
# escalate to the full run.
|
|
case "$file" in
|
|
hack/e2e-chainsaw/_lib/*|hack/e2e-chainsaw/.chainsaw.yaml)
|
|
trigger_full=1
|
|
continue ;;
|
|
hack/e2e-chainsaw/*/*.disabled)
|
|
# A suite parked as chainsaw-test.yaml.disabled is registered nowhere and
|
|
# executed by nothing, so an edit to it cannot regress a test. Matched
|
|
# before the per-suite rule below, which reads the suite name off the
|
|
# directory and ignores the suffix: the name it derives matches no
|
|
# existing suite, the intersection at the bottom empties, and the
|
|
# safety net for a genuinely unclassified selection escalates to the
|
|
# full run — the most expensive outcome, bought by the one file class
|
|
# that provably cannot affect anything. Inert is the classification;
|
|
# the safety net keeps its job for paths no rule has looked at.
|
|
continue ;;
|
|
hack/e2e-chainsaw/*/*)
|
|
app=$(echo "$file" | sed -nE 's,^hack/e2e-chainsaw/([^/]+)/.*,\1,p')
|
|
selected_apps="$selected_apps $app"
|
|
trigger_any=1
|
|
continue ;;
|
|
examples/backups/*/*)
|
|
# The etcd, postgres, mariadb and clickhouse backup round-trip tests
|
|
# execute the example scripts under examples/backups/<app>/ as their
|
|
# harness, so an edit there must run that app's suite. This mapping is
|
|
# also why a round-trip Test belongs in its app's own suite dir: put it
|
|
# in a dir whose name does not match examples/backups/<app>/ and an edit
|
|
# to the harness selects a suite that cannot exercise it. A dir with no
|
|
# matching suite is a docs-only demo and stays ignored (adding it to
|
|
# selected_apps would empty the final intersection and trip the
|
|
# full-suite safety net).
|
|
app=$(echo "$file" | sed -nE 's,^examples/backups/([^/]+)/.*,\1,p')
|
|
if echo "$all_apps" | grep -Fxq "$app"; then
|
|
selected_apps="$selected_apps $app"
|
|
trigger_any=1
|
|
fi
|
|
continue ;;
|
|
esac
|
|
|
|
# 3. Full-suite trigger
|
|
if echo "$file" | grep -qE "$full_suite_pattern"; then
|
|
trigger_full=1
|
|
continue
|
|
fi
|
|
|
|
# 4. Explicitly inert: repo meta, agent config, non-e2e workflows, fixtures.
|
|
if echo "$file" | grep -qE "$inert_config_pattern"; then
|
|
continue
|
|
fi
|
|
|
|
# 5. Component change: lookup in PackageSource graph
|
|
rel=$(echo "$file" | sed -nE 's,^packages/(apps|system|extra)/([^/]+)/.*,\1/\2,p')
|
|
if [ -n "$rel" ]; then
|
|
src=$(echo "$OWNERS" | awk -v p="$rel" -F'\t' '$1==p {print $2}')
|
|
if [ -n "$src" ]; then
|
|
# Keep this path's owning sources together as one comma-joined group. The
|
|
# unit coverage is decided over is the changed PATH, not the single
|
|
# source: system/postgres-operator belongs to two PackageSources, one of
|
|
# which reaches no suite, and deciding per source would escalate on that
|
|
# half and run everything for every change to it.
|
|
selected_groups="$selected_groups $(echo "$src" | paste -sd , -)"
|
|
trigger_any=1
|
|
else
|
|
# Inside packages/ but no graph entry — be conservative.
|
|
trigger_full=1
|
|
fi
|
|
continue
|
|
fi
|
|
|
|
# 6. Unclassified. Escalate instead of ignoring: an empty selection makes both
|
|
# e2e lanes skip Chainsaw and post "E2E Tests" green, so a path nobody has
|
|
# classified must fail safe, not fail open (#3392). Add genuinely inert
|
|
# paths to inert_config_pattern rather than relaxing this.
|
|
echo "select-e2e: unclassified path '$file' — escalating to the full suite (classify it in hack/select-e2e.sh, see #3392)" >&2
|
|
trigger_full=1
|
|
done < "$CHANGED"
|
|
|
|
if [ "$trigger_full" = 1 ]; then
|
|
escalate_to_full_suite
|
|
fi
|
|
|
|
if [ "$trigger_any" = 0 ]; then
|
|
exit 0 # nothing to run
|
|
fi
|
|
|
|
# Deduplicate a space-separated suite list; keep only names that are suites.
|
|
intersect_suites() {
|
|
echo "$1" | tr ' ' '\n' | sort -u | grep -v '^$' | while read -r app; do
|
|
if echo "$all_apps" | grep -Fxq "$app"; then
|
|
echo "$app"
|
|
fi
|
|
done | paste -sd ' ' -
|
|
}
|
|
|
|
# Transitive closure over reverse-deps from the given PackageSource names, every
|
|
# reached source mapped through src_to_suites. Emits candidate suite names, not
|
|
# yet filtered — pass the result through intersect_suites.
|
|
#
|
|
# POSIX sh has no `local`, so this scribbles on all_sources/new/final. Its one
|
|
# call site invokes it inside $( ), which contains the damage; a direct call
|
|
# would clobber the caller's variables. A test pins that there is still exactly
|
|
# one call.
|
|
resolve_suites() {
|
|
all_sources="$*"
|
|
while :; do
|
|
new=""
|
|
for s in $all_sources; do
|
|
deps=$(echo "$REVERSE" | awk -v src="$s" -F'\t' '$1==src {print $2}')
|
|
for d in $deps; do
|
|
case " $all_sources " in *" $d "*) ;; *) new="$new $d";; esac
|
|
done
|
|
done
|
|
[ -z "$new" ] && break
|
|
all_sources="$all_sources $new"
|
|
done
|
|
|
|
final=""
|
|
for s in $all_sources; do
|
|
final="$final $(src_to_suites "${s#cozystack.}")"
|
|
done
|
|
echo "$final"
|
|
}
|
|
|
|
# Escalation is a property of the changed path, not of the whole diff. A path
|
|
# that reaches no runnable suite is covered by nothing and forces the full run
|
|
# on its own account, whatever the rest of the diff selected.
|
|
#
|
|
# Reading that off an empty FINAL selection instead — which is what the old
|
|
# safety net at the bottom did, and still does as a backstop — let any other
|
|
# changed path that contributed a suite name swallow the escalation, with
|
|
# nothing in the output to record that it happened (#3330). The shape that hits
|
|
# it is ordinary rather than exotic: change a platform component, adjust the
|
|
# Chainsaw test next to it, and the component's escalation disappears behind
|
|
# that one suite.
|
|
group_suites=""
|
|
for g in $(echo "$selected_groups" | tr ' ' '\n' | sort -u); do
|
|
s=$(intersect_suites "$(resolve_suites "$(echo "$g" | tr ',' ' ')")")
|
|
if [ -z "$s" ]; then
|
|
# Say which changed path bought the full run. Every other escalation here
|
|
# names its cause on stderr, and this one is the common case rather than the
|
|
# rare one — most packages reach no suite of their own — so without a line
|
|
# "why did this pull request run everything" has no answer in the log. The
|
|
# sources are printed rather than the path because they are what the walk
|
|
# started from; the path that owns them is the one this group came from in
|
|
# the classification loop above.
|
|
echo "select-e2e: no runnable suite covers $(echo "$g" | tr ',' ' ') — escalating to the full suite" >&2
|
|
escalate_to_full_suite
|
|
fi
|
|
group_suites="$group_suites $s"
|
|
done
|
|
|
|
# Union of the graph-selected suites and the directly-selected ones from
|
|
# per-suite edits. Walking the graph once per group rather than once over all of
|
|
# them costs an extra walk per changed path and buys the escalation above; the
|
|
# answer is unchanged, because reverse reachability distributes over union, so
|
|
# the union of the per-group results is the set the single walk produced.
|
|
final_apps=$(intersect_suites "$group_suites $selected_apps")
|
|
|
|
# Backstop. Every graph path above either escalates or contributes a suite that
|
|
# exists, so what still reaches this is a per-suite edit naming a directory that
|
|
# holds no chainsaw-test.yaml — shared material beside _lib/, or a suite nested
|
|
# deeper than the depth-2 scan looks. Selecting nothing for those would skip
|
|
# E2E outright, so failing towards the full suite is the only safe way to be
|
|
# wrong here.
|
|
if [ -z "$final_apps" ]; then
|
|
escalate_to_full_suite
|
|
fi
|
|
|
|
echo "$final_apps"
|